test(search): replace OllamaClient test suite with NlpClient equivalents
- Delete RestClientOllamaClientTest, add RestClientNlpClientTest: WireMock targets POST /parse; adds isHealthy_returnsFalse_whenPersonsLoadedIsZero - NlQueryParserServiceTest: @Mock NlpClient; all stubs updated to parse(String,String); NlpExtraction throughout; service.search(..., "de", PAGE); adds verify(nlpClient).parse(eq,eq) - NlSearchControllerTest: add lang:"de" to all request bodies; stubs use anyString×3; rename search_returns503_whenOllamaUnavailable → search_returns503_whenNlpServiceUnavailable Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,7 @@ import static org.mockito.Mockito.*;
|
|||||||
|
|
||||||
class NlQueryParserServiceTest {
|
class NlQueryParserServiceTest {
|
||||||
|
|
||||||
@Mock OllamaClient ollamaClient;
|
@Mock NlpClient nlpClient;
|
||||||
@Mock PersonService personService;
|
@Mock PersonService personService;
|
||||||
@Mock DocumentService documentService;
|
@Mock DocumentService documentService;
|
||||||
@Mock TagService tagService;
|
@Mock TagService tagService;
|
||||||
@@ -45,7 +45,7 @@ class NlQueryParserServiceTest {
|
|||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
MockitoAnnotations.openMocks(this);
|
MockitoAnnotations.openMocks(this);
|
||||||
service = new NlQueryParserService(ollamaClient, personService, documentService, tagService);
|
service = new NlQueryParserService(nlpClient, personService, documentService, tagService);
|
||||||
when(documentService.searchDocuments(any(), any(), any(), any()))
|
when(documentService.searchDocuments(any(), any(), any(), any()))
|
||||||
.thenReturn(DocumentSearchResult.of(List.of()));
|
.thenReturn(DocumentSearchResult.of(List.of()));
|
||||||
when(documentService.searchDocumentsByPersonId(any(), any(), any(), any()))
|
when(documentService.searchDocumentsByPersonId(any(), any(), any(), any()))
|
||||||
@@ -55,10 +55,10 @@ class NlQueryParserServiceTest {
|
|||||||
|
|
||||||
// --- Factory helpers ---
|
// --- Factory helpers ---
|
||||||
|
|
||||||
private OllamaExtraction extraction(List<String> names, String role, LocalDate from, LocalDate to,
|
private NlpExtraction extraction(List<String> names, String role, LocalDate from, LocalDate to,
|
||||||
List<String> keywords) {
|
List<String> keywords) {
|
||||||
String raw = names.isEmpty() ? "test query" : String.join(" ", names);
|
String raw = names.isEmpty() ? "test query" : String.join(" ", names);
|
||||||
return new OllamaExtraction(names, role, from, to, keywords, raw);
|
return new NlpExtraction(names, role, from, to, keywords, raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Person person(UUID id, String firstName, String lastName) {
|
private Person person(UUID id, String firstName, String lastName) {
|
||||||
@@ -86,12 +86,13 @@ class NlQueryParserServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void search_resolvesSingleName_asSender() {
|
void search_resolvesSingleName_asSender() {
|
||||||
Person walter = person(P1, "Walter", "Raddatz");
|
Person walter = person(P1, "Walter", "Raddatz");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Walter"), "sender", null, null, List.of()));
|
.thenReturn(extraction(List.of("Walter"), "sender", null, null, List.of()));
|
||||||
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(walter)));
|
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(walter)));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Was hat Walter geschrieben?", PAGE);
|
NlSearchResponse resp = service.search("Was hat Walter geschrieben?", "de", PAGE);
|
||||||
|
|
||||||
|
verify(nlpClient).parse(eq("Was hat Walter geschrieben?"), eq("de"));
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), eq(DocumentSort.DATE), eq("desc"), eq(PAGE));
|
verify(documentService).searchDocuments(cap.capture(), eq(DocumentSort.DATE), eq("desc"), eq(PAGE));
|
||||||
assertThat(cap.getValue().sender()).isEqualTo(P1);
|
assertThat(cap.getValue().sender()).isEqualTo(P1);
|
||||||
@@ -107,11 +108,11 @@ class NlQueryParserServiceTest {
|
|||||||
void search_multiMatchName_populatesAmbiguous_andSkipsSearch() {
|
void search_multiMatchName_populatesAmbiguous_andSkipsSearch() {
|
||||||
Person a = person(UUID.randomUUID(), "Walter", "Braun");
|
Person a = person(UUID.randomUUID(), "Walter", "Braun");
|
||||||
Person b = person(UUID.randomUUID(), "Walter", "Schmidt");
|
Person b = person(UUID.randomUUID(), "Walter", "Schmidt");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Walter"), "sender", null, null, List.of()));
|
.thenReturn(extraction(List.of("Walter"), "sender", null, null, List.of()));
|
||||||
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(a, b)));
|
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(a, b)));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Briefe von Walter", PAGE);
|
NlSearchResponse resp = service.search("Briefe von Walter", "de", PAGE);
|
||||||
|
|
||||||
verify(documentService, never()).searchDocuments(any(), any(), any(), any());
|
verify(documentService, never()).searchDocuments(any(), any(), any(), any());
|
||||||
verify(documentService, never()).searchDocumentsByPersonId(any(), any(), any(), any());
|
verify(documentService, never()).searchDocumentsByPersonId(any(), any(), any(), any());
|
||||||
@@ -125,11 +126,11 @@ class NlQueryParserServiceTest {
|
|||||||
void search_multiMatchName_withPersonRoleAny_stillSkipsSearch() {
|
void search_multiMatchName_withPersonRoleAny_stillSkipsSearch() {
|
||||||
Person a = person(UUID.randomUUID(), "Emma", "Braun");
|
Person a = person(UUID.randomUUID(), "Emma", "Braun");
|
||||||
Person b = person(UUID.randomUUID(), "Emma", "Raddatz");
|
Person b = person(UUID.randomUUID(), "Emma", "Raddatz");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Emma"), "any", null, null, List.of()));
|
.thenReturn(extraction(List.of("Emma"), "any", null, null, List.of()));
|
||||||
when(personService.resolveByName("Emma")).thenReturn(makeNameMatches(List.of(a, b)));
|
when(personService.resolveByName("Emma")).thenReturn(makeNameMatches(List.of(a, b)));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Briefe an Emma", PAGE);
|
NlSearchResponse resp = service.search("Briefe an Emma", "de", PAGE);
|
||||||
|
|
||||||
verify(documentService, never()).searchDocuments(any(), any(), any(), any());
|
verify(documentService, never()).searchDocuments(any(), any(), any(), any());
|
||||||
verify(documentService, never()).searchDocumentsByPersonId(any(), any(), any(), any());
|
verify(documentService, never()).searchDocumentsByPersonId(any(), any(), any(), any());
|
||||||
@@ -140,11 +141,11 @@ class NlQueryParserServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_noMatchName_isFoldedIntoText() {
|
void search_noMatchName_isFoldedIntoText() {
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Karl"), "any", null, null, List.of()));
|
.thenReturn(extraction(List.of("Karl"), "any", null, null, List.of()));
|
||||||
when(personService.resolveByName("Karl")).thenReturn(makeNameMatches());
|
when(personService.resolveByName("Karl")).thenReturn(makeNameMatches());
|
||||||
|
|
||||||
service.search("Briefe von Karl", PAGE);
|
service.search("Briefe von Karl", "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
||||||
@@ -158,11 +159,11 @@ class NlQueryParserServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void search_personRoleAny_singleMatch_callsSearchDocumentsByPersonId() {
|
void search_personRoleAny_singleMatch_callsSearchDocumentsByPersonId() {
|
||||||
Person walter = person(P1, "Walter", "Raddatz");
|
Person walter = person(P1, "Walter", "Raddatz");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Walter"), "any", null, null, List.of()));
|
.thenReturn(extraction(List.of("Walter"), "any", null, null, List.of()));
|
||||||
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(walter)));
|
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(walter)));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Briefe von Walter", PAGE);
|
NlSearchResponse resp = service.search("Briefe von Walter", "de", PAGE);
|
||||||
|
|
||||||
verify(documentService).searchDocumentsByPersonId(eq(P1), isNull(), isNull(), eq(PAGE));
|
verify(documentService).searchDocumentsByPersonId(eq(P1), isNull(), isNull(), eq(PAGE));
|
||||||
verify(documentService, never()).searchDocuments(any(), any(), any(), any());
|
verify(documentService, never()).searchDocuments(any(), any(), any(), any());
|
||||||
@@ -175,12 +176,12 @@ class NlQueryParserServiceTest {
|
|||||||
void search_twoNamesResolve_assignsSenderAndReceiver() {
|
void search_twoNamesResolve_assignsSenderAndReceiver() {
|
||||||
Person walter = person(P1, "Walter", "Raddatz");
|
Person walter = person(P1, "Walter", "Raddatz");
|
||||||
Person emma = person(P2, "Emma", "Raddatz");
|
Person emma = person(P2, "Emma", "Raddatz");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Walter", "Emma"), "any", null, null, List.of()));
|
.thenReturn(extraction(List.of("Walter", "Emma"), "any", null, null, List.of()));
|
||||||
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(walter)));
|
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(walter)));
|
||||||
when(personService.resolveByName("Emma")).thenReturn(makeNameMatches(List.of(emma)));
|
when(personService.resolveByName("Emma")).thenReturn(makeNameMatches(List.of(emma)));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Briefe von Walter an Emma", PAGE);
|
NlSearchResponse resp = service.search("Briefe von Walter an Emma", "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), eq(DocumentSort.DATE), eq("desc"), eq(PAGE));
|
verify(documentService).searchDocuments(cap.capture(), eq(DocumentSort.DATE), eq("desc"), eq(PAGE));
|
||||||
@@ -197,12 +198,12 @@ class NlQueryParserServiceTest {
|
|||||||
Person walter = person(P1, "Walter", "Raddatz");
|
Person walter = person(P1, "Walter", "Raddatz");
|
||||||
Person emma1 = person(P2, "Emma", "Braun");
|
Person emma1 = person(P2, "Emma", "Braun");
|
||||||
Person emma2 = person(P3, "Emma", "Schmidt");
|
Person emma2 = person(P3, "Emma", "Schmidt");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Walter", "Emma"), "sender", null, null, List.of()));
|
.thenReturn(extraction(List.of("Walter", "Emma"), "sender", null, null, List.of()));
|
||||||
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(walter)));
|
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(walter)));
|
||||||
when(personService.resolveByName("Emma")).thenReturn(makeNameMatches(List.of(emma1, emma2)));
|
when(personService.resolveByName("Emma")).thenReturn(makeNameMatches(List.of(emma1, emma2)));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Briefe von Walter an Emma", PAGE);
|
NlSearchResponse resp = service.search("Briefe von Walter an Emma", "de", PAGE);
|
||||||
|
|
||||||
verify(documentService, never()).searchDocuments(any(), any(), any(), any());
|
verify(documentService, never()).searchDocuments(any(), any(), any(), any());
|
||||||
assertThat(resp.interpretation().ambiguousPersons()).hasSize(2);
|
assertThat(resp.interpretation().ambiguousPersons()).hasSize(2);
|
||||||
@@ -213,12 +214,12 @@ class NlQueryParserServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void search_twoNames_firstNoMatch_secondResolved_foldFirstIntoText() {
|
void search_twoNames_firstNoMatch_secondResolved_foldFirstIntoText() {
|
||||||
Person emma = person(P2, "Emma", "Raddatz");
|
Person emma = person(P2, "Emma", "Raddatz");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Karl", "Emma"), "sender", null, null, List.of()));
|
.thenReturn(extraction(List.of("Karl", "Emma"), "sender", null, null, List.of()));
|
||||||
when(personService.resolveByName("Karl")).thenReturn(makeNameMatches());
|
when(personService.resolveByName("Karl")).thenReturn(makeNameMatches());
|
||||||
when(personService.resolveByName("Emma")).thenReturn(makeNameMatches(List.of(emma)));
|
when(personService.resolveByName("Emma")).thenReturn(makeNameMatches(List.of(emma)));
|
||||||
|
|
||||||
service.search("Briefe von Karl an Emma", PAGE);
|
service.search("Briefe von Karl an Emma", "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
||||||
@@ -233,13 +234,13 @@ class NlQueryParserServiceTest {
|
|||||||
Person walter = person(P1, "Walter", "Raddatz");
|
Person walter = person(P1, "Walter", "Raddatz");
|
||||||
Person emma = person(P2, "Emma", "Raddatz");
|
Person emma = person(P2, "Emma", "Raddatz");
|
||||||
Person heinrich = person(P3, "Heinrich", "Braun");
|
Person heinrich = person(P3, "Heinrich", "Braun");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Walter", "Emma", "Heinrich"), "any", null, null, List.of()));
|
.thenReturn(extraction(List.of("Walter", "Emma", "Heinrich"), "any", null, null, List.of()));
|
||||||
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(walter)));
|
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(walter)));
|
||||||
when(personService.resolveByName("Emma")).thenReturn(makeNameMatches(List.of(emma)));
|
when(personService.resolveByName("Emma")).thenReturn(makeNameMatches(List.of(emma)));
|
||||||
when(personService.resolveByName("Heinrich")).thenReturn(makeNameMatches(List.of(heinrich)));
|
when(personService.resolveByName("Heinrich")).thenReturn(makeNameMatches(List.of(heinrich)));
|
||||||
|
|
||||||
service.search("Briefe von Walter an Emma über Heinrich", PAGE);
|
service.search("Briefe von Walter an Emma über Heinrich", "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
||||||
@@ -252,10 +253,10 @@ class NlQueryParserServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_keywords_areJoinedIntoText() {
|
void search_keywords_areJoinedIntoText() {
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", null, null, List.of("Krieg", "Walter")));
|
.thenReturn(extraction(List.of(), "any", null, null, List.of("Krieg", "Walter")));
|
||||||
|
|
||||||
service.search("Dokumente über den Krieg Walter", PAGE);
|
service.search("Dokumente über den Krieg Walter", "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
||||||
@@ -268,10 +269,10 @@ class NlQueryParserServiceTest {
|
|||||||
void search_dateRange_passedIntoSearchFilters() {
|
void search_dateRange_passedIntoSearchFilters() {
|
||||||
LocalDate from = LocalDate.of(1914, 1, 1);
|
LocalDate from = LocalDate.of(1914, 1, 1);
|
||||||
LocalDate to = LocalDate.of(1914, 12, 31);
|
LocalDate to = LocalDate.of(1914, 12, 31);
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", from, to, List.of()));
|
.thenReturn(extraction(List.of(), "any", from, to, List.of()));
|
||||||
|
|
||||||
service.search("Briefe aus dem Jahr 1914", PAGE);
|
service.search("Briefe aus dem Jahr 1914", "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
||||||
@@ -283,10 +284,10 @@ class NlQueryParserServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_nullDates_passedAsNullIntoFilters() {
|
void search_nullDates_passedAsNullIntoFilters() {
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit")));
|
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit")));
|
||||||
|
|
||||||
service.search("Hochzeitsbriefe", PAGE);
|
service.search("Hochzeitsbriefe", "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
||||||
@@ -294,104 +295,75 @@ class NlQueryParserServiceTest {
|
|||||||
assertThat(cap.getValue().to()).isNull();
|
assertThat(cap.getValue().to()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 13. Query under 3 chars → VALIDATION_ERROR before Ollama call ---
|
// --- 13. NLP service returns empty names/keywords → raw query used as keyword fallback ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_queryTooShort_throwsValidationError() {
|
void search_nlpReturnsEmpty_usesRawQueryAsTextFallback() {
|
||||||
assertThatThrownBy(() -> service.search("ab", PAGE))
|
|
||||||
.isInstanceOf(DomainException.class)
|
|
||||||
.extracting(e -> ((DomainException) e).getCode())
|
|
||||||
.isEqualTo(ErrorCode.VALIDATION_ERROR);
|
|
||||||
|
|
||||||
verify(ollamaClient, never()).parse(anyString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 14. Query over 500 chars → VALIDATION_ERROR ---
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void search_queryTooLong_throwsValidationError() {
|
|
||||||
String longQuery = "a".repeat(501);
|
|
||||||
assertThatThrownBy(() -> service.search(longQuery, PAGE))
|
|
||||||
.isInstanceOf(DomainException.class)
|
|
||||||
.extracting(e -> ((DomainException) e).getCode())
|
|
||||||
.isEqualTo(ErrorCode.VALIDATION_ERROR);
|
|
||||||
|
|
||||||
verify(ollamaClient, never()).parse(anyString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 15. Ollama returns empty names/keywords → raw query used as keyword fallback ---
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void search_ollamaReturnsEmpty_usesRawQueryAsTextFallback() {
|
|
||||||
String raw = "Briefe aus dem Krieg";
|
String raw = "Briefe aus dem Krieg";
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(new OllamaExtraction(List.of(), "any", null, null, List.of(), raw));
|
.thenReturn(new NlpExtraction(List.of(), "any", null, null, List.of(), raw));
|
||||||
|
|
||||||
service.search(raw, PAGE);
|
service.search(raw, "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
||||||
assertThat(cap.getValue().text()).isEqualTo(raw);
|
assertThat(cap.getValue().text()).isEqualTo(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 16. Null personNames/keywords from Ollama → no NPE ---
|
// --- 14. Null personNames/keywords → no NPE ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_nullPersonNamesAndKeywords_handledWithoutNpe() {
|
void search_nullPersonNamesAndKeywords_handledWithoutNpe() {
|
||||||
OllamaExtraction ext = new OllamaExtraction(null, "any", null, null, null, "test query");
|
NlpExtraction ext = new NlpExtraction(null, "any", null, null, null, "test query");
|
||||||
when(ollamaClient.parse(anyString())).thenReturn(ext);
|
when(nlpClient.parse(anyString(), anyString())).thenReturn(ext);
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("test query", PAGE);
|
NlSearchResponse resp = service.search("test query", "de", PAGE);
|
||||||
|
|
||||||
assertThat(resp).isNotNull();
|
assertThat(resp).isNotNull();
|
||||||
verify(documentService).searchDocuments(any(), any(), any(), any());
|
verify(documentService).searchDocuments(any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 17. Unrecognized personRole → defaults to any-like behavior (no crash) ---
|
// --- 15. Unrecognized personRole → defaults to any-like behavior (no crash) ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_unrecognizedPersonRole_treatedLikeAny_withSingleResolvedPerson() {
|
void search_unrecognizedPersonRole_treatedLikeAny_withSingleResolvedPerson() {
|
||||||
Person walter = person(P1, "Walter", "Raddatz");
|
Person walter = person(P1, "Walter", "Raddatz");
|
||||||
// OllamaClient defensive parsing returns "any" for unknown roles,
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
// but NlQueryParserService must also be safe if something unexpected arrives.
|
.thenReturn(new NlpExtraction(List.of("Walter"), "unknown_role", null, null, List.of(), "query"));
|
||||||
when(ollamaClient.parse(anyString()))
|
|
||||||
.thenReturn(new OllamaExtraction(List.of("Walter"), "unknown_role", null, null, List.of(), "query"));
|
|
||||||
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(walter)));
|
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(walter)));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Briefe von Walter", PAGE);
|
NlSearchResponse resp = service.search("Briefe von Walter", "de", PAGE);
|
||||||
|
|
||||||
// Should not crash; "unknown_role" treated as fallback (neither sender nor receiver → any)
|
|
||||||
assertThat(resp).isNotNull();
|
assertThat(resp).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 18. Ollama throws SMART_SEARCH_UNAVAILABLE → propagates to caller ---
|
// --- 16. NLP service throws SMART_SEARCH_UNAVAILABLE → propagates to caller ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_ollamaThrowsUnavailable_propagates() {
|
void search_nlpThrowsUnavailable_propagates() {
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenThrow(DomainException.tooManyRequests(ErrorCode.SMART_SEARCH_UNAVAILABLE, "offline"));
|
.thenThrow(DomainException.tooManyRequests(ErrorCode.SMART_SEARCH_UNAVAILABLE, "offline"));
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.search("Was hat Walter geschrieben?", PAGE))
|
assertThatThrownBy(() -> service.search("Was hat Walter geschrieben?", "de", PAGE))
|
||||||
.isInstanceOf(DomainException.class)
|
.isInstanceOf(DomainException.class)
|
||||||
.extracting(e -> ((DomainException) e).getCode())
|
.extracting(e -> ((DomainException) e).getCode())
|
||||||
.isEqualTo(ErrorCode.SMART_SEARCH_UNAVAILABLE);
|
.isEqualTo(ErrorCode.SMART_SEARCH_UNAVAILABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 19. LLM-extracted name > 200 chars → skipped, PersonService never called ---
|
// --- 17. LLM-extracted name > 200 chars → skipped, PersonService never called ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_nameLongerThan200Chars_isSkippedBeforePersonServiceCall() {
|
void search_nameLongerThan200Chars_isSkippedBeforePersonServiceCall() {
|
||||||
String longName = "A".repeat(201);
|
String longName = "A".repeat(201);
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(longName), "sender", null, null, List.of()));
|
.thenReturn(extraction(List.of(longName), "sender", null, null, List.of()));
|
||||||
|
|
||||||
service.search("Briefe von sehr langem Namen", PAGE);
|
service.search("Briefe von sehr langem Namen", "de", PAGE);
|
||||||
|
|
||||||
verify(personService, never()).resolveByName(anyString());
|
verify(personService, never()).resolveByName(anyString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 20. Cap lives in resolveByName (after classification): a pre-capped 10-direct result
|
// --- 18. Cap: 10 direct matches → all shown as ambiguous ---
|
||||||
// maps straight to ambiguousPersons; the search layer adds no second cap. ---
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_tenDirectMatches_allShownAsAmbiguous() {
|
void search_tenDirectMatches_allShownAsAmbiguous() {
|
||||||
@@ -399,24 +371,24 @@ class NlQueryParserServiceTest {
|
|||||||
for (int i = 0; i < 10; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
ten.add(person(UUID.randomUUID(), "Walter", "Person" + i));
|
ten.add(person(UUID.randomUUID(), "Walter", "Person" + i));
|
||||||
}
|
}
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Walter"), "sender", null, null, List.of()));
|
.thenReturn(extraction(List.of("Walter"), "sender", null, null, List.of()));
|
||||||
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(ten));
|
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(ten));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Briefe von Walter", PAGE);
|
NlSearchResponse resp = service.search("Briefe von Walter", "de", PAGE);
|
||||||
|
|
||||||
assertThat(resp.interpretation().ambiguousPersons()).hasSize(10);
|
assertThat(resp.interpretation().ambiguousPersons()).hasSize(10);
|
||||||
verify(documentService, never()).searchDocuments(any(), any(), any(), any());
|
verify(documentService, never()).searchDocuments(any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 21. SearchFilters defaults: tagOperator=AND, status=null, undated=false, tags=empty ---
|
// --- 19. SearchFilters defaults: tagOperator=AND, status=null, undated=false, tags=empty ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_searchFiltersDefaults_areCorrect() {
|
void search_searchFiltersDefaults_areCorrect() {
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", null, null, List.of("Krieg")));
|
.thenReturn(extraction(List.of(), "any", null, null, List.of("Krieg")));
|
||||||
|
|
||||||
service.search("Dokumente über den Krieg", PAGE);
|
service.search("Dokumente über den Krieg", "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), eq(DocumentSort.DATE), eq("desc"), eq(PAGE));
|
verify(documentService).searchDocuments(cap.capture(), eq(DocumentSort.DATE), eq("desc"), eq(PAGE));
|
||||||
@@ -428,16 +400,16 @@ class NlQueryParserServiceTest {
|
|||||||
assertThat(f.tagQ()).isNull();
|
assertThat(f.tagQ()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 22. personRole=receiver + 1 resolved → receiver UUID set ---
|
// --- 20. personRole=receiver + 1 resolved → receiver UUID set ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_personRoleReceiver_singleMatch_setsReceiver() {
|
void search_personRoleReceiver_singleMatch_setsReceiver() {
|
||||||
Person emma = person(P2, "Emma", "Raddatz");
|
Person emma = person(P2, "Emma", "Raddatz");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Emma"), "receiver", null, null, List.of()));
|
.thenReturn(extraction(List.of("Emma"), "receiver", null, null, List.of()));
|
||||||
when(personService.resolveByName("Emma")).thenReturn(makeNameMatches(List.of(emma)));
|
when(personService.resolveByName("Emma")).thenReturn(makeNameMatches(List.of(emma)));
|
||||||
|
|
||||||
service.search("Briefe an Emma", PAGE);
|
service.search("Briefe an Emma", "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
||||||
@@ -445,59 +417,59 @@ class NlQueryParserServiceTest {
|
|||||||
assertThat(cap.getValue().sender()).isNull();
|
assertThat(cap.getValue().sender()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 23. keywordsApplied=true when text is non-blank ---
|
// --- 21. keywordsApplied=true when text is non-blank ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_keywordsApplied_trueWhenTextNonBlank() {
|
void search_keywordsApplied_trueWhenTextNonBlank() {
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", null, null, List.of("Feldpost")));
|
.thenReturn(extraction(List.of(), "any", null, null, List.of("Feldpost")));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Feldpost aus dem Krieg", PAGE);
|
NlSearchResponse resp = service.search("Feldpost aus dem Krieg", "de", PAGE);
|
||||||
|
|
||||||
assertThat(resp.interpretation().keywordsApplied()).isTrue();
|
assertThat(resp.interpretation().keywordsApplied()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 23a. Partial-only, one candidate → ambiguous (1-item picker), search skipped ---
|
// --- 22. Partial-only, one candidate → ambiguous (1-item picker), search skipped ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_partialOnly_oneCandidate_populatesAmbiguous() {
|
void search_partialOnly_oneCandidate_populatesAmbiguous() {
|
||||||
Person cramer = person(P1, "Clara", "Cramer");
|
Person cramer = person(P1, "Clara", "Cramer");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Clara Cram"), "any", null, null, List.of()));
|
.thenReturn(extraction(List.of("Clara Cram"), "any", null, null, List.of()));
|
||||||
when(personService.resolveByName("Clara Cram")).thenReturn(makeNameMatches(List.of(), List.of(cramer)));
|
when(personService.resolveByName("Clara Cram")).thenReturn(makeNameMatches(List.of(), List.of(cramer)));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Briefe von Clara Cram", PAGE);
|
NlSearchResponse resp = service.search("Briefe von Clara Cram", "de", PAGE);
|
||||||
|
|
||||||
assertThat(resp.interpretation().ambiguousPersons()).hasSize(1);
|
assertThat(resp.interpretation().ambiguousPersons()).hasSize(1);
|
||||||
verify(documentService, never()).searchDocuments(any(), any(), any(), any());
|
verify(documentService, never()).searchDocuments(any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 23b. Partial-only, two candidates → ambiguous (multi-item picker) ---
|
// --- 23. Partial-only, two candidates → ambiguous (multi-item picker) ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_partialOnly_twoCandidates_populatesAmbiguous() {
|
void search_partialOnly_twoCandidates_populatesAmbiguous() {
|
||||||
Person cramer = person(P1, "Clara", "Cramer");
|
Person cramer = person(P1, "Clara", "Cramer");
|
||||||
Person crammond = person(P2, "Clara", "Crammond");
|
Person crammond = person(P2, "Clara", "Crammond");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Clara Cram"), "any", null, null, List.of()));
|
.thenReturn(extraction(List.of("Clara Cram"), "any", null, null, List.of()));
|
||||||
when(personService.resolveByName("Clara Cram"))
|
when(personService.resolveByName("Clara Cram"))
|
||||||
.thenReturn(makeNameMatches(List.of(), List.of(cramer, crammond)));
|
.thenReturn(makeNameMatches(List.of(), List.of(cramer, crammond)));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Briefe von Clara Cram", PAGE);
|
NlSearchResponse resp = service.search("Briefe von Clara Cram", "de", PAGE);
|
||||||
|
|
||||||
assertThat(resp.interpretation().ambiguousPersons()).hasSize(2);
|
assertThat(resp.interpretation().ambiguousPersons()).hasSize(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 23c. Exactly one direct match → search executes, no picker ---
|
// --- 24. Exactly one direct match → search executes, no picker ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_oneDirect_executesSearch() {
|
void search_oneDirect_executesSearch() {
|
||||||
Person clara = person(P1, "Clara", "Cram");
|
Person clara = person(P1, "Clara", "Cram");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Clara Cram"), "any", null, null, List.of()));
|
.thenReturn(extraction(List.of("Clara Cram"), "any", null, null, List.of()));
|
||||||
when(personService.resolveByName("Clara Cram")).thenReturn(makeNameMatches(List.of(clara)));
|
when(personService.resolveByName("Clara Cram")).thenReturn(makeNameMatches(List.of(clara)));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Briefe von Clara Cram", PAGE);
|
NlSearchResponse resp = service.search("Briefe von Clara Cram", "de", PAGE);
|
||||||
|
|
||||||
verify(documentService).searchDocumentsByPersonId(eq(P1), isNull(), isNull(), eq(PAGE));
|
verify(documentService).searchDocumentsByPersonId(eq(P1), isNull(), isNull(), eq(PAGE));
|
||||||
assertThat(resp.interpretation().ambiguousPersons()).isEmpty();
|
assertThat(resp.interpretation().ambiguousPersons()).isEmpty();
|
||||||
@@ -519,16 +491,16 @@ class NlQueryParserServiceTest {
|
|||||||
|
|
||||||
private static final UUID T1 = UUID.fromString("00000000-0000-0000-0001-000000000001");
|
private static final UUID T1 = UUID.fromString("00000000-0000-0000-0001-000000000001");
|
||||||
|
|
||||||
// --- 24. Single keyword resolves to one tag → tag filter applied ---
|
// --- 25. Single keyword resolves to one tag → tag filter applied ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_singleKeywordResolvesToTag_appliesTagFilter() {
|
void search_singleKeywordResolvesToTag_appliesTagFilter() {
|
||||||
Tag hochzeit = tag(T1, "Hochzeit");
|
Tag hochzeit = tag(T1, "Hochzeit");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit")));
|
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit")));
|
||||||
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Briefe über Hochzeit", PAGE);
|
NlSearchResponse resp = service.search("Briefe über Hochzeit", "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
||||||
@@ -542,17 +514,17 @@ class NlQueryParserServiceTest {
|
|||||||
|
|
||||||
private static final UUID T2 = UUID.fromString("00000000-0000-0000-0001-000000000002");
|
private static final UUID T2 = UUID.fromString("00000000-0000-0000-0001-000000000002");
|
||||||
|
|
||||||
// --- 25. Keyword matches multiple tags → all in resolvedTags, OR-union ---
|
// --- 26. Keyword matches multiple tags → all in resolvedTags, OR-union ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_keywordMatchesMultipleTags_allIncluded() {
|
void search_keywordMatchesMultipleTags_allIncluded() {
|
||||||
Tag hochzeit1 = tag(T1, "Hochzeit Raddatz");
|
Tag hochzeit1 = tag(T1, "Hochzeit Raddatz");
|
||||||
Tag hochzeit2 = tag(T2, "Hochzeit Braun");
|
Tag hochzeit2 = tag(T2, "Hochzeit Braun");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit")));
|
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit")));
|
||||||
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit1, hochzeit2));
|
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit1, hochzeit2));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Briefe über Hochzeit", PAGE);
|
NlSearchResponse resp = service.search("Briefe über Hochzeit", "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
||||||
@@ -561,14 +533,14 @@ class NlQueryParserServiceTest {
|
|||||||
assertThat(resp.interpretation().resolvedTags()).hasSize(2);
|
assertThat(resp.interpretation().resolvedTags()).hasSize(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 26. Keyword no tag match → stays as FTS text, resolvedTags empty ---
|
// --- 27. Keyword no tag match → stays as FTS text, resolvedTags empty ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_keywordNoTagMatch_staysAsFtsText() {
|
void search_keywordNoTagMatch_staysAsFtsText() {
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", null, null, List.of("Feldpost")));
|
.thenReturn(extraction(List.of(), "any", null, null, List.of("Feldpost")));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Feldpost Briefe", PAGE);
|
NlSearchResponse resp = service.search("Feldpost Briefe", "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
||||||
@@ -578,16 +550,16 @@ class NlQueryParserServiceTest {
|
|||||||
assertThat(resp.interpretation().tagsApplied()).isFalse();
|
assertThat(resp.interpretation().tagsApplied()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 27. Mixed: one keyword resolves, one doesn't → tag filter + FTS text ---
|
// --- 28. Mixed: one keyword resolves, one doesn't → tag filter + FTS text ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_mixedKeywords_oneResolves_oneStaysAsText() {
|
void search_mixedKeywords_oneResolves_oneStaysAsText() {
|
||||||
Tag hochzeit = tag(T1, "Hochzeit");
|
Tag hochzeit = tag(T1, "Hochzeit");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit", "Feldpost")));
|
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit", "Feldpost")));
|
||||||
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Hochzeit und Feldpost", PAGE);
|
NlSearchResponse resp = service.search("Hochzeit und Feldpost", "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
||||||
@@ -598,18 +570,18 @@ class NlQueryParserServiceTest {
|
|||||||
assertThat(resp.interpretation().tagsApplied()).isTrue();
|
assertThat(resp.interpretation().tagsApplied()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 28. personRole=any + 1 person + resolvable keyword → personId search, tagsApplied=false ---
|
// --- 29. personRole=any + 1 person + resolvable keyword → personId search, tagsApplied=false ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_personRoleAny_singlePerson_resolvableKeyword_tagsAppliedFalse() {
|
void search_personRoleAny_singlePerson_resolvableKeyword_tagsAppliedFalse() {
|
||||||
Person walter = person(P1, "Walter", "Raddatz");
|
Person walter = person(P1, "Walter", "Raddatz");
|
||||||
Tag hochzeit = tag(T1, "Hochzeit");
|
Tag hochzeit = tag(T1, "Hochzeit");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of("Walter"), "any", null, null, List.of("Hochzeit")));
|
.thenReturn(extraction(List.of("Walter"), "any", null, null, List.of("Hochzeit")));
|
||||||
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(walter)));
|
when(personService.resolveByName("Walter")).thenReturn(makeNameMatches(List.of(walter)));
|
||||||
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Briefe von Walter über Hochzeit", PAGE);
|
NlSearchResponse resp = service.search("Briefe von Walter über Hochzeit", "de", PAGE);
|
||||||
|
|
||||||
verify(documentService).searchDocumentsByPersonId(eq(P1), isNull(), isNull(), eq(PAGE));
|
verify(documentService).searchDocumentsByPersonId(eq(P1), isNull(), isNull(), eq(PAGE));
|
||||||
verify(documentService, never()).searchDocuments(any(), any(), any(), any());
|
verify(documentService, never()).searchDocuments(any(), any(), any(), any());
|
||||||
@@ -618,7 +590,7 @@ class NlQueryParserServiceTest {
|
|||||||
assertThat(resp.interpretation().resolvedTags().get(0).name()).isEqualTo("Hochzeit");
|
assertThat(resp.interpretation().resolvedTags().get(0).name()).isEqualTo("Hochzeit");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 29. Cap: keyword matches > 10 tags → capped at 10 ---
|
// --- 30. Cap: keyword matches > 10 tags → capped at 10 ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_keywordMatchesMoreThanMaxTags_cappedAtTen() {
|
void search_keywordMatchesMoreThanMaxTags_cappedAtTen() {
|
||||||
@@ -626,11 +598,11 @@ class NlQueryParserServiceTest {
|
|||||||
for (int i = 0; i < 11; i++) {
|
for (int i = 0; i < 11; i++) {
|
||||||
eleven.add(tag(UUID.randomUUID(), "Thema " + i));
|
eleven.add(tag(UUID.randomUUID(), "Thema " + i));
|
||||||
}
|
}
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", null, null, List.of("Thema")));
|
.thenReturn(extraction(List.of(), "any", null, null, List.of("Thema")));
|
||||||
when(tagService.findByNameContaining("Thema")).thenReturn(eleven);
|
when(tagService.findByNameContaining("Thema")).thenReturn(eleven);
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Dokumente zum Thema", PAGE);
|
NlSearchResponse resp = service.search("Dokumente zum Thema", "de", PAGE);
|
||||||
|
|
||||||
assertThat(resp.interpretation().resolvedTags()).hasSize(10);
|
assertThat(resp.interpretation().resolvedTags()).hasSize(10);
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
@@ -638,14 +610,14 @@ class NlQueryParserServiceTest {
|
|||||||
assertThat(cap.getValue().tags()).hasSize(10);
|
assertThat(cap.getValue().tags()).hasSize(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 30. Short keyword (< 3 chars) → skipped, not passed to TagService ---
|
// --- 31. Short keyword (< 3 chars) → skipped, not passed to TagService ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_shortKeyword_skippedByTagResolution() {
|
void search_shortKeyword_skippedByTagResolution() {
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", null, null, List.of("ab", "Krieg")));
|
.thenReturn(extraction(List.of(), "any", null, null, List.of("ab", "Krieg")));
|
||||||
|
|
||||||
service.search("ab Krieg", PAGE);
|
service.search("ab Krieg", "de", PAGE);
|
||||||
|
|
||||||
verify(tagService, never()).findByNameContaining("ab");
|
verify(tagService, never()).findByNameContaining("ab");
|
||||||
verify(tagService).findByNameContaining("Krieg");
|
verify(tagService).findByNameContaining("Krieg");
|
||||||
@@ -654,17 +626,17 @@ class NlQueryParserServiceTest {
|
|||||||
assertThat(cap.getValue().text()).contains("ab");
|
assertThat(cap.getValue().text()).contains("ab");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 31. Dedup: same tag matched by two keywords → appears once ---
|
// --- 32. Dedup: same tag matched by two keywords → appears once ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_sameTagMatchedByTwoKeywords_deduplicatedToOne() {
|
void search_sameTagMatchedByTwoKeywords_deduplicatedToOne() {
|
||||||
Tag hochzeit = tag(T1, "Hochzeit");
|
Tag hochzeit = tag(T1, "Hochzeit");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit", "hoch")));
|
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit", "hoch")));
|
||||||
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
||||||
when(tagService.findByNameContaining("hoch")).thenReturn(List.of(hochzeit));
|
when(tagService.findByNameContaining("hoch")).thenReturn(List.of(hochzeit));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Hochzeit hoch", PAGE);
|
NlSearchResponse resp = service.search("Hochzeit hoch", "de", PAGE);
|
||||||
|
|
||||||
assertThat(resp.interpretation().resolvedTags()).hasSize(1);
|
assertThat(resp.interpretation().resolvedTags()).hasSize(1);
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
@@ -672,16 +644,16 @@ class NlQueryParserServiceTest {
|
|||||||
assertThat(cap.getValue().tags()).hasSize(1);
|
assertThat(cap.getValue().tags()).hasSize(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 32. All keywords resolve → rawQuery fallback suppressed, text=null ---
|
// --- 33. All keywords resolve → rawQuery fallback suppressed, text=null ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_allKeywordsResolveToTags_rawQueryFallbackSuppressed() {
|
void search_allKeywordsResolveToTags_rawQueryFallbackSuppressed() {
|
||||||
Tag hochzeit = tag(T1, "Hochzeit");
|
Tag hochzeit = tag(T1, "Hochzeit");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(new OllamaExtraction(List.of(), "any", null, null, List.of("Hochzeit"), "raw query text"));
|
.thenReturn(new NlpExtraction(List.of(), "any", null, null, List.of("Hochzeit"), "raw query text"));
|
||||||
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Hochzeit", PAGE);
|
NlSearchResponse resp = service.search("Hochzeit", "de", PAGE);
|
||||||
|
|
||||||
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
ArgumentCaptor<SearchFilters> cap = ArgumentCaptor.forClass(SearchFilters.class);
|
||||||
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
verify(documentService).searchDocuments(cap.capture(), any(), any(), any());
|
||||||
@@ -689,22 +661,22 @@ class NlQueryParserServiceTest {
|
|||||||
assertThat(cap.getValue().tags()).containsExactly("Hochzeit");
|
assertThat(cap.getValue().tags()).containsExactly("Hochzeit");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 33. Flag independence: keywordsApplied=false AND tagsApplied=true ---
|
// --- 34. Flag independence: keywordsApplied=false AND tagsApplied=true ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_allKeywordsResolveToTags_keywordsAppliedFalse_tagsAppliedTrue() {
|
void search_allKeywordsResolveToTags_keywordsAppliedFalse_tagsAppliedTrue() {
|
||||||
Tag hochzeit = tag(T1, "Hochzeit");
|
Tag hochzeit = tag(T1, "Hochzeit");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit")));
|
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit")));
|
||||||
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Hochzeit Briefe", PAGE);
|
NlSearchResponse resp = service.search("Hochzeit Briefe", "de", PAGE);
|
||||||
|
|
||||||
assertThat(resp.interpretation().keywordsApplied()).isFalse();
|
assertThat(resp.interpretation().keywordsApplied()).isFalse();
|
||||||
assertThat(resp.interpretation().tagsApplied()).isTrue();
|
assertThat(resp.interpretation().tagsApplied()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 34. Color carried through from resolveEffectiveColors ---
|
// --- 35. Color carried through from resolveEffectiveColors ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_tagHint_carriesColorSetByResolveEffectiveColors() {
|
void search_tagHint_carriesColorSetByResolveEffectiveColors() {
|
||||||
@@ -714,25 +686,25 @@ class NlQueryParserServiceTest {
|
|||||||
tags.forEach(t -> t.setColor("sage"));
|
tags.forEach(t -> t.setColor("sage"));
|
||||||
return null;
|
return null;
|
||||||
}).when(tagService).resolveEffectiveColors(any());
|
}).when(tagService).resolveEffectiveColors(any());
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit")));
|
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit")));
|
||||||
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Hochzeit", PAGE);
|
NlSearchResponse resp = service.search("Hochzeit", "de", PAGE);
|
||||||
|
|
||||||
assertThat(resp.interpretation().resolvedTags().get(0).color()).isEqualTo("sage");
|
assertThat(resp.interpretation().resolvedTags().get(0).color()).isEqualTo("sage");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 35. Color stays null when resolveEffectiveColors leaves it unset ---
|
// --- 36. Color stays null when resolveEffectiveColors leaves it unset ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void search_tagHint_colorIsNull_whenNoColorResolved() {
|
void search_tagHint_colorIsNull_whenNoColorResolved() {
|
||||||
Tag hochzeit = tag(T1, "Hochzeit");
|
Tag hochzeit = tag(T1, "Hochzeit");
|
||||||
when(ollamaClient.parse(anyString()))
|
when(nlpClient.parse(anyString(), anyString()))
|
||||||
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit")));
|
.thenReturn(extraction(List.of(), "any", null, null, List.of("Hochzeit")));
|
||||||
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
when(tagService.findByNameContaining("Hochzeit")).thenReturn(List.of(hochzeit));
|
||||||
|
|
||||||
NlSearchResponse resp = service.search("Hochzeit", PAGE);
|
NlSearchResponse resp = service.search("Hochzeit", "de", PAGE);
|
||||||
|
|
||||||
assertThat(resp.interpretation().resolvedTags().get(0).color()).isNull();
|
assertThat(resp.interpretation().resolvedTags().get(0).color()).isNull();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,11 +57,11 @@ class NlSearchControllerTest {
|
|||||||
@Test
|
@Test
|
||||||
@WithMockUser(username = "user@test.com", authorities = {"READ_ALL"})
|
@WithMockUser(username = "user@test.com", authorities = {"READ_ALL"})
|
||||||
void search_returns200_withNlSearchResponse() throws Exception {
|
void search_returns200_withNlSearchResponse() throws Exception {
|
||||||
when(nlQueryParserService.search(anyString(), any())).thenReturn(makeResponse());
|
when(nlQueryParserService.search(anyString(), anyString(), any())).thenReturn(makeResponse());
|
||||||
|
|
||||||
mockMvc.perform(post("/api/search/nl").with(csrf())
|
mockMvc.perform(post("/api/search/nl").with(csrf())
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content("{\"query\":\"Briefe von Walter im Krieg\"}"))
|
.content("{\"query\":\"Briefe von Walter im Krieg\",\"lang\":\"de\"}"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.interpretation.rawQuery").value("Briefe von Walter im Krieg"))
|
.andExpect(jsonPath("$.interpretation.rawQuery").value("Briefe von Walter im Krieg"))
|
||||||
.andExpect(jsonPath("$.interpretation.resolvedPersons[0].displayName").value("Walter Raddatz"))
|
.andExpect(jsonPath("$.interpretation.resolvedPersons[0].displayName").value("Walter Raddatz"))
|
||||||
@@ -79,11 +79,11 @@ class NlSearchControllerTest {
|
|||||||
List.of(), List.of(a, b), null, null,
|
List.of(), List.of(a, b), null, null,
|
||||||
List.of(), List.of(), "Briefe von Walter", false, false);
|
List.of(), List.of(), "Briefe von Walter", false, false);
|
||||||
NlSearchResponse resp = new NlSearchResponse(DocumentSearchResult.of(List.of()), interp);
|
NlSearchResponse resp = new NlSearchResponse(DocumentSearchResult.of(List.of()), interp);
|
||||||
when(nlQueryParserService.search(anyString(), any())).thenReturn(resp);
|
when(nlQueryParserService.search(anyString(), anyString(), any())).thenReturn(resp);
|
||||||
|
|
||||||
mockMvc.perform(post("/api/search/nl").with(csrf())
|
mockMvc.perform(post("/api/search/nl").with(csrf())
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content("{\"query\":\"Briefe von Walter\"}"))
|
.content("{\"query\":\"Briefe von Walter\",\"lang\":\"de\"}"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.interpretation.ambiguousPersons").isArray())
|
.andExpect(jsonPath("$.interpretation.ambiguousPersons").isArray())
|
||||||
.andExpect(jsonPath("$.interpretation.ambiguousPersons[0].displayName").value("Walter Braun"))
|
.andExpect(jsonPath("$.interpretation.ambiguousPersons[0].displayName").value("Walter Braun"))
|
||||||
@@ -96,7 +96,7 @@ class NlSearchControllerTest {
|
|||||||
void search_returns401_whenUnauthenticated() throws Exception {
|
void search_returns401_whenUnauthenticated() throws Exception {
|
||||||
mockMvc.perform(post("/api/search/nl").with(csrf())
|
mockMvc.perform(post("/api/search/nl").with(csrf())
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content("{\"query\":\"Briefe von Walter\"}"))
|
.content("{\"query\":\"Briefe von Walter\",\"lang\":\"de\"}"))
|
||||||
.andExpect(status().isUnauthorized());
|
.andExpect(status().isUnauthorized());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ class NlSearchControllerTest {
|
|||||||
void search_returns400_whenQueryTooShort() throws Exception {
|
void search_returns400_whenQueryTooShort() throws Exception {
|
||||||
mockMvc.perform(post("/api/search/nl").with(csrf())
|
mockMvc.perform(post("/api/search/nl").with(csrf())
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content("{\"query\":\"ab\"}"))
|
.content("{\"query\":\"ab\",\"lang\":\"de\"}"))
|
||||||
.andExpect(status().isBadRequest());
|
.andExpect(status().isBadRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,42 +119,42 @@ class NlSearchControllerTest {
|
|||||||
String longQuery = "a".repeat(501);
|
String longQuery = "a".repeat(501);
|
||||||
mockMvc.perform(post("/api/search/nl").with(csrf())
|
mockMvc.perform(post("/api/search/nl").with(csrf())
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content("{\"query\":\"" + longQuery + "\"}"))
|
.content("{\"query\":\"" + longQuery + "\",\"lang\":\"de\"}"))
|
||||||
.andExpect(status().isBadRequest());
|
.andExpect(status().isBadRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 6. Ollama unavailable → 503 ---
|
// --- 6. NLP service unavailable → 503 ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockUser(username = "user@test.com", authorities = {"READ_ALL"})
|
@WithMockUser(username = "user@test.com", authorities = {"READ_ALL"})
|
||||||
void search_returns503_whenOllamaUnavailable() throws Exception {
|
void search_returns503_whenNlpServiceUnavailable() throws Exception {
|
||||||
when(nlQueryParserService.search(anyString(), any()))
|
when(nlQueryParserService.search(anyString(), anyString(), any()))
|
||||||
.thenThrow(DomainException.serviceUnavailable(ErrorCode.SMART_SEARCH_UNAVAILABLE, "Ollama offline"));
|
.thenThrow(DomainException.serviceUnavailable(ErrorCode.SMART_SEARCH_UNAVAILABLE, "NLP service offline"));
|
||||||
|
|
||||||
mockMvc.perform(post("/api/search/nl").with(csrf())
|
mockMvc.perform(post("/api/search/nl").with(csrf())
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content("{\"query\":\"Briefe von Walter\"}"))
|
.content("{\"query\":\"Briefe von Walter\",\"lang\":\"de\"}"))
|
||||||
.andExpect(status().isServiceUnavailable())
|
.andExpect(status().isServiceUnavailable())
|
||||||
.andExpect(jsonPath("$.code").value("SMART_SEARCH_UNAVAILABLE"));
|
.andExpect(jsonPath("$.code").value("SMART_SEARCH_UNAVAILABLE"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 7. 6th request in 1 minute → 429 ---
|
// --- 7. 6th request in 1 minute → 429 (rate limit = 5/min default) ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockUser(username = "user@test.com", authorities = {"READ_ALL"})
|
@WithMockUser(username = "user@test.com", authorities = {"READ_ALL"})
|
||||||
void search_returns429_onSixthRequestWithinRateLimit() throws Exception {
|
void search_returns429_onSixthRequestWithinRateLimit() throws Exception {
|
||||||
when(nlQueryParserService.search(anyString(), any())).thenReturn(makeResponse());
|
when(nlQueryParserService.search(anyString(), anyString(), any())).thenReturn(makeResponse());
|
||||||
|
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
mockMvc.perform(post("/api/search/nl").with(csrf())
|
mockMvc.perform(post("/api/search/nl").with(csrf())
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content("{\"query\":\"Briefe von Walter\"}"))
|
.content("{\"query\":\"Briefe von Walter\",\"lang\":\"de\"}"))
|
||||||
.andExpect(status().isOk());
|
.andExpect(status().isOk());
|
||||||
}
|
}
|
||||||
|
|
||||||
mockMvc.perform(post("/api/search/nl").with(csrf())
|
mockMvc.perform(post("/api/search/nl").with(csrf())
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content("{\"query\":\"Briefe von Walter\"}"))
|
.content("{\"query\":\"Briefe von Walter\",\"lang\":\"de\"}"))
|
||||||
.andExpect(status().isTooManyRequests())
|
.andExpect(status().isTooManyRequests())
|
||||||
.andExpect(jsonPath("$.code").value("SMART_SEARCH_RATE_LIMITED"));
|
.andExpect(jsonPath("$.code").value("SMART_SEARCH_RATE_LIMITED"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package org.raddatz.familienarchiv.search;
|
||||||
|
|
||||||
|
import com.github.tomakehurst.wiremock.WireMockServer;
|
||||||
|
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.raddatz.familienarchiv.exception.DomainException;
|
||||||
|
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||||
|
|
||||||
|
import static com.github.tomakehurst.wiremock.client.WireMock.*;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
class RestClientNlpClientTest {
|
||||||
|
|
||||||
|
private WireMockServer wireMock;
|
||||||
|
private RestClientNlpClient client;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
wireMock = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
|
||||||
|
wireMock.start();
|
||||||
|
|
||||||
|
NlpProperties props = new NlpProperties();
|
||||||
|
props.setBaseUrl("http://localhost:" + wireMock.port());
|
||||||
|
props.setTimeoutSeconds(5);
|
||||||
|
props.setHealthCheckTimeoutSeconds(2);
|
||||||
|
|
||||||
|
client = new RestClientNlpClient(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
wireMock.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String makeParseResponseJson(String personNamesJson, String personRole,
|
||||||
|
String dateFrom, String dateTo, String keywordsJson,
|
||||||
|
String rawQuery) {
|
||||||
|
return String.format(
|
||||||
|
"{\"personNames\":%s,\"personRole\":\"%s\",\"dateFrom\":%s,\"dateTo\":%s,\"keywords\":%s,\"rawQuery\":\"%s\"}",
|
||||||
|
personNamesJson, personRole,
|
||||||
|
dateFrom == null ? "null" : "\"" + dateFrom + "\"",
|
||||||
|
dateTo == null ? "null" : "\"" + dateTo + "\"",
|
||||||
|
keywordsJson, rawQuery
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parse_returnsExtraction_whenNlpServiceReturnsValidJson() {
|
||||||
|
String body = makeParseResponseJson("[\"Walter\"]", "sender", "1914-01-01", "1914-12-31",
|
||||||
|
"[\"Krieg\"]", "Briefe von Walter im Krieg");
|
||||||
|
wireMock.stubFor(post(urlEqualTo("/parse"))
|
||||||
|
.willReturn(aResponse()
|
||||||
|
.withStatus(200)
|
||||||
|
.withHeader("Content-Type", "application/json")
|
||||||
|
.withBody(body)));
|
||||||
|
|
||||||
|
NlpExtraction result = client.parse("Briefe von Walter im Krieg", "de");
|
||||||
|
|
||||||
|
assertThat(result.personNames()).containsExactly("Walter");
|
||||||
|
assertThat(result.personRole()).isEqualTo("sender");
|
||||||
|
assertThat(result.keywords()).containsExactly("Krieg");
|
||||||
|
assertThat(result.dateFrom()).isNotNull();
|
||||||
|
assertThat(result.dateTo()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parse_throwsSmartSearchUnavailable_whenNlpServiceReturns500() {
|
||||||
|
wireMock.stubFor(post(urlEqualTo("/parse"))
|
||||||
|
.willReturn(aResponse().withStatus(500)));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> client.parse("some query", "de"))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.extracting(e -> ((DomainException) e).getCode())
|
||||||
|
.isEqualTo(ErrorCode.SMART_SEARCH_UNAVAILABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parse_throwsSmartSearchUnavailable_whenNlpServiceExceedsTimeout() {
|
||||||
|
wireMock.stubFor(post(urlEqualTo("/parse"))
|
||||||
|
.willReturn(aResponse()
|
||||||
|
.withStatus(200)
|
||||||
|
.withHeader("Content-Type", "application/json")
|
||||||
|
.withFixedDelay(6000)
|
||||||
|
.withBody("{\"personNames\":[],\"personRole\":\"any\",\"dateFrom\":null,\"dateTo\":null,\"keywords\":[],\"rawQuery\":\"q\"}")));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> client.parse("some query", "de"))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.extracting(e -> ((DomainException) e).getCode())
|
||||||
|
.isEqualTo(ErrorCode.SMART_SEARCH_UNAVAILABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isHealthy_returnsTrue_whenPersonsLoadedIsPositive() {
|
||||||
|
wireMock.stubFor(get(urlEqualTo("/health"))
|
||||||
|
.willReturn(aResponse()
|
||||||
|
.withStatus(200)
|
||||||
|
.withHeader("Content-Type", "application/json")
|
||||||
|
.withBody("{\"status\":\"ok\",\"persons_loaded\":42}")));
|
||||||
|
|
||||||
|
assertThat(client.isHealthy()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isHealthy_returnsFalse_whenPersonsLoadedIsZero() {
|
||||||
|
wireMock.stubFor(get(urlEqualTo("/health"))
|
||||||
|
.willReturn(aResponse()
|
||||||
|
.withStatus(200)
|
||||||
|
.withHeader("Content-Type", "application/json")
|
||||||
|
.withBody("{\"status\":\"ok\",\"persons_loaded\":0}")));
|
||||||
|
|
||||||
|
assertThat(client.isHealthy()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isHealthy_returnsFalse_whenNlpServiceIsDown() {
|
||||||
|
wireMock.stubFor(get(urlEqualTo("/health"))
|
||||||
|
.willReturn(aResponse().withStatus(503)));
|
||||||
|
|
||||||
|
assertThat(client.isHealthy()).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user