mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-07-02 16:47:30 +02:00
Context sensitive search (#2102)
Extend global search to search context-sensitive in repositories and namespaces.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user