Context sensitive search (#2102)

Extend global search to search context-sensitive in repositories and namespaces.
This commit is contained in:
Eduard Heimbuch
2022-08-04 11:29:05 +02:00
parent 6c82142643
commit 550ebefd93
34 changed files with 1061 additions and 308 deletions

View File

@@ -41,11 +41,14 @@ import sonia.scm.repository.Namespace;
import sonia.scm.repository.NamespaceManager;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.search.SearchEngine;
import sonia.scm.search.SearchableType;
import sonia.scm.web.RestDispatcher;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Optional;
import static com.google.inject.util.Providers.of;
@@ -58,6 +61,7 @@ import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class NamespaceRootResourceTest {
@@ -68,6 +72,11 @@ class NamespaceRootResourceTest {
NamespaceManager namespaceManager;
@Mock
Subject subject;
@Mock
SearchEngine searchEngine;
@Mock
SearchableType searchableType;
RestDispatcher dispatcher = new RestDispatcher();
MockHttpResponse response = new MockHttpResponse();
@@ -89,7 +98,7 @@ class NamespaceRootResourceTest {
@BeforeEach
void setUpResources() {
NamespaceToNamespaceDtoMapper namespaceMapper = new NamespaceToNamespaceDtoMapper(links);
NamespaceToNamespaceDtoMapper namespaceMapper = new NamespaceToNamespaceDtoMapper(links, searchEngine);
NamespaceCollectionToDtoMapper namespaceCollectionToDtoMapper = new NamespaceCollectionToDtoMapper(namespaceMapper, links);
RepositoryPermissionCollectionToDtoMapper repositoryPermissionCollectionToDtoMapper = new RepositoryPermissionCollectionToDtoMapper(repositoryPermissionToRepositoryPermissionDtoMapper, links);
RepositoryPermissionDtoToRepositoryPermissionMapperImpl dtoToModelMapper = new RepositoryPermissionDtoToRepositoryPermissionMapperImpl();
@@ -111,6 +120,12 @@ class NamespaceRootResourceTest {
lenient().when(namespaceManager.get("space")).thenReturn(Optional.of(spaceNamespace));
}
@BeforeEach
void mockEmptySearchableTypes() {
lenient().when(searchEngine.getSearchableTypes())
.thenReturn(List.of(searchableType));
}
@Nested
class WithoutSpecialPermission {
@@ -165,6 +180,21 @@ class NamespaceRootResourceTest {
assertThat(response.getStatus()).isEqualTo(403);
}
@Test
void shouldReturnSearchLinks() throws URISyntaxException, UnsupportedEncodingException {
when(searchableType.limitableToNamespace()).thenReturn(true);
when(searchableType.getName()).thenReturn("crew");
MockHttpRequest request = MockHttpRequest.get("/" + NamespaceRootResource.NAMESPACE_PATH_V2 + "space");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentAsString())
.contains("\"search\":[{\"href\":\"/v2/search/query/space/crew\",\"name\":\"crew\"}]")
.contains("\"searchableTypes\":{\"href\":\"/v2/search/searchableTypes/space\"}");
}
}
@Nested

View File

@@ -68,6 +68,7 @@ import sonia.scm.repository.api.BundleCommandBuilder;
import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.search.SearchEngine;
import sonia.scm.user.User;
import sonia.scm.web.RestDispatcher;
import sonia.scm.web.VndMediaType;
@@ -163,6 +164,8 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
private HealthCheckService healthCheckService;
@Mock
private ExportNotificationHandler notificationHandler;
@Mock
private SearchEngine searchEngine;
@Captor
private ArgumentCaptor<Predicate<Repository>> filterCaptor;

View File

@@ -45,8 +45,11 @@ import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.repository.api.ScmProtocol;
import sonia.scm.search.SearchEngine;
import sonia.scm.search.SearchableType;
import java.net.URI;
import java.util.List;
import java.util.Set;
import static java.util.Collections.singletonList;
@@ -57,6 +60,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import static sonia.scm.repository.HealthCheckFailure.templated;
@@ -90,6 +94,8 @@ public class RepositoryToRepositoryDtoMapperTest {
private HealthCheckService healthCheckService;
@Mock
private SCMContextProvider scmContextProvider;
@Mock
private SearchEngine searchEngine;
@InjectMocks
private RepositoryToRepositoryDtoMapperImpl mapper;
@@ -382,6 +388,26 @@ public class RepositoryToRepositoryDtoMapperTest {
.isNotPresent();
}
@Test
public void shouldCreateSearchLink() {
SearchableType searchableType = mock(SearchableType.class);
when(searchableType.getName()).thenReturn("crew");
when(searchableType.limitableToRepository()).thenReturn(true);
when(searchEngine.getSearchableTypes()).thenReturn(List.of(searchableType));
Repository testRepository = createTestRepository();
RepositoryDto dto = mapper.map(testRepository);
assertThat(dto.getLinks().getLinkBy("search"))
.get()
.extracting("name", "href")
.containsExactly("crew", "http://example.com/base/v2/search/query/testspace/test/crew");
assertThat(dto.getLinks().getLinkBy("searchableTypes"))
.get()
.extracting("href")
.isEqualTo("http://example.com/base/v2/search/searchableTypes/testspace/test");
}
private ScmProtocol mockProtocol(String type, String protocol) {
return new MockScmProtocol(type, protocol);
}

View File

@@ -39,11 +39,13 @@ import org.mapstruct.factory.Mappers;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryCoordinates;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.search.Hit;
import sonia.scm.search.QueryBuilder;
import sonia.scm.search.QueryCountResult;
import sonia.scm.search.QueryResult;
import sonia.scm.search.SearchEngine;
@@ -66,7 +68,10 @@ import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@@ -83,12 +88,6 @@ class SearchResourceTest {
@Mock
private HalEnricherRegistry enricherRegistry;
@Mock
private SearchableType searchableTypeOne;
@Mock
private SearchableType searchableTypeTwo;
@BeforeEach
void setUpDispatcher() {
ScmPathInfoStore scmPathInfoStore = new ScmPathInfoStore();
@@ -101,27 +100,70 @@ class SearchResourceTest {
SearchableTypeMapper searchableTypeMapper = Mappers.getMapper(SearchableTypeMapper.class);
queryResultMapper.setRegistry(enricherRegistry);
SearchResource resource = new SearchResource(
searchEngine, queryResultMapper, searchableTypeMapper
searchEngine, queryResultMapper, searchableTypeMapper, repositoryManager
);
dispatcher = new RestDispatcher();
dispatcher.addSingletonResource(resource);
}
@Test
void shouldReturnSearchableTypes() throws URISyntaxException {
when(searchEngine.getSearchableTypes()).thenReturn(Lists.list(searchableTypeOne, searchableTypeTwo));
when(searchableTypeOne.getName()).thenReturn("Type One");
when(searchableTypeTwo.getName()).thenReturn("Type Two");
@Nested
class SearchableTypes {
MockHttpRequest request = MockHttpRequest.get("/v2/search/searchableTypes");
JsonMockHttpResponse response = new JsonMockHttpResponse();
dispatcher.invoke(request, response);
@Mock
private SearchableType searchableTypeOne;
@Mock
private SearchableType searchableTypeTwo;
JsonNode contentAsJson = response.getContentAsJson();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(contentAsJson.isArray()).isTrue();
assertThat(contentAsJson.get(0).get("name").asText()).isEqualTo("Type One");
assertThat(contentAsJson.get(1).get("name").asText()).isEqualTo("Type Two");
@Test
void shouldReturnGlobalSearchableTypes() throws URISyntaxException {
when(searchEngine.getSearchableTypes()).thenReturn(Lists.list(searchableTypeOne, searchableTypeTwo));
when(searchableTypeOne.getName()).thenReturn("Type One");
when(searchableTypeTwo.getName()).thenReturn("Type Two");
MockHttpRequest request = MockHttpRequest.get("/v2/search/searchableTypes");
JsonMockHttpResponse response = new JsonMockHttpResponse();
dispatcher.invoke(request, response);
JsonNode contentAsJson = response.getContentAsJson();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(contentAsJson.isArray()).isTrue();
assertThat(contentAsJson.get(0).get("name").asText()).isEqualTo("Type One");
assertThat(contentAsJson.get(1).get("name").asText()).isEqualTo("Type Two");
}
@Test
void shouldReturnSearchableTypesForNamespace() throws URISyntaxException {
when(searchEngine.getSearchableTypes()).thenReturn(Lists.list(searchableTypeOne, searchableTypeTwo));
when(searchableTypeOne.getName()).thenReturn("Type One");
when(searchableTypeOne.limitableToNamespace()).thenReturn(true);
MockHttpRequest request = MockHttpRequest.get("/v2/search/searchableTypes/space");
JsonMockHttpResponse response = new JsonMockHttpResponse();
dispatcher.invoke(request, response);
JsonNode contentAsJson = response.getContentAsJson();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(contentAsJson.isArray()).isTrue();
assertThat(contentAsJson.get(0).get("name").asText()).isEqualTo("Type One");
assertThat(contentAsJson.get(1)).isNull();
}
@Test
void shouldReturnSearchableTypesForRepository() throws URISyntaxException {
when(searchEngine.getSearchableTypes()).thenReturn(Lists.list(searchableTypeOne, searchableTypeTwo));
when(searchableTypeOne.getName()).thenReturn("Type One");
when(searchableTypeOne.limitableToRepository()).thenReturn(true);
MockHttpRequest request = MockHttpRequest.get("/v2/search/searchableTypes/hitchhiker/hog");
JsonMockHttpResponse response = new JsonMockHttpResponse();
dispatcher.invoke(request, response);
JsonNode contentAsJson = response.getContentAsJson();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(contentAsJson.isArray()).isTrue();
assertThat(contentAsJson.get(0).get("name").asText()).isEqualTo("Type One");
assertThat(contentAsJson.get(1)).isNull();
}
}
@Test
@@ -292,7 +334,73 @@ class SearchResourceTest {
assertThat(repositoryNode.get("type").asText()).isEqualTo(heartOfGold.getType());
assertThat(repositoryNode.get("_links").get("self").get("href").asText()).isEqualTo("/v2/repositories/hitchhiker/HeartOfGold");
}
}
@Nested
class WithScope {
@Mock
private QueryBuilder<Object> internalQueryBuilder;
private final Repository repository1 = new Repository("1", "git", "space", "hog");
private final Repository repository2 = new Repository("2", "git", "space", "hitchhiker");
private final Repository repository3 = new Repository("3", "git", "earth", "42");
@BeforeEach
void mockRepositories() {
lenient().when(repositoryManager.getAll())
.thenReturn(
List.of(
repository1,
repository2,
repository3
)
);
lenient().when(repositoryManager.get(new NamespaceAndName("space", "hog")))
.thenReturn(repository1);
}
@BeforeEach
void mockSearchResult() {
when(
searchEngine.forType("string")
.search()
.start(0)
.limit(10)
).thenReturn(internalQueryBuilder);
when(internalQueryBuilder.filter(any())).thenReturn(internalQueryBuilder);
when(
internalQueryBuilder.execute("Hello")
).thenReturn(result(2L, "Hello", "Hello Again"));
}
@Test
void shouldReturnResultsScopedToNamespace() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/v2/search/query/space/string?q=Hello");
JsonMockHttpResponse response = new JsonMockHttpResponse();
dispatcher.invoke(request, response);
JsonNode hits = response.getContentAsJson().get("_embedded").get("hits");
assertThat(hits.size()).isEqualTo(2);
verify(internalQueryBuilder).filter(repository1);
verify(internalQueryBuilder).filter(repository2);
verify(internalQueryBuilder, never()).filter(repository3);
}
@Test
void shouldReturnResultsScopedToRepository() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/v2/search/query/space/hog/string?q=Hello");
JsonMockHttpResponse response = new JsonMockHttpResponse();
dispatcher.invoke(request, response);
JsonNode hits = response.getContentAsJson().get("_embedded").get("hits");
assertThat(hits.size()).isEqualTo(2);
verify(internalQueryBuilder).filter(repository1);
verify(internalQueryBuilder, never()).filter(repository2);
verify(internalQueryBuilder, never()).filter(repository3);
}
}
private void assertLink(JsonNode links, String self, String s) {