From b2a072bdb80c168034122bf5a6b5e40f9f15706e Mon Sep 17 00:00:00 2001 From: Alex Klimenko Date: Thu, 7 Aug 2025 14:56:14 +0200 Subject: [PATCH] test: Enhance Document and DocumentBuilder test coverage with edge cases and validation tests Signed-off-by: Alex Klimenko --- .../ai/document/DocumentBuilderTests.java | 116 ++++++++++++++ .../ai/document/DocumentTests.java | 146 ++++++++++++++++++ 2 files changed, 262 insertions(+) diff --git a/spring-ai-commons/src/test/java/org/springframework/ai/document/DocumentBuilderTests.java b/spring-ai-commons/src/test/java/org/springframework/ai/document/DocumentBuilderTests.java index 484bbdb4026..edf60a5b104 100644 --- a/spring-ai-commons/src/test/java/org/springframework/ai/document/DocumentBuilderTests.java +++ b/spring-ai-commons/src/test/java/org/springframework/ai/document/DocumentBuilderTests.java @@ -290,4 +290,120 @@ void testBuilderReuse() { assertThat(doc2.getMetadata()).containsEntry("key", "value2"); } + @Test + void testMediaDocumentWithoutText() { + Media media = getMedia(); + Document document = this.builder.media(media).build(); + + assertThat(document.getMedia()).isEqualTo(media); + assertThat(document.getText()).isNull(); + } + + @Test + void testTextDocumentWithoutMedia() { + Document document = this.builder.text("test content").build(); + + assertThat(document.getText()).isEqualTo("test content"); + assertThat(document.getMedia()).isNull(); + } + + @Test + void testOverwritingMediaWithNull() { + Media media = getMedia(); + Document document = this.builder.media(media).media(null).text("fallback").build(); + + assertThat(document.getMedia()).isNull(); + } + + @Test + void testMetadataWithSpecialCharacterKeys() { + Document document = this.builder.text("test") + .metadata("key-with-dashes", "value1") + .metadata("key.with.dots", "value2") + .metadata("key_with_underscores", "value3") + .metadata("key with spaces", "value4") + .build(); + + assertThat(document.getMetadata()).containsEntry("key-with-dashes", "value1") + .containsEntry("key.with.dots", "value2") + .containsEntry("key_with_underscores", "value3") + .containsEntry("key with spaces", "value4"); + } + + @Test + void testBuilderStateIsolation() { + // Configure first builder state + this.builder.text("first").metadata("shared", "first"); + + // Create first document + Document doc1 = this.builder.build(); + + // Modify builder for second document + this.builder.text("second").metadata("shared", "second"); + + // Create second document + Document doc2 = this.builder.build(); + + // Verify first document wasn't affected by subsequent changes + assertThat(doc1.getText()).isEqualTo("first"); + assertThat(doc1.getMetadata()).containsEntry("shared", "first"); + + assertThat(doc2.getText()).isEqualTo("second"); + assertThat(doc2.getMetadata()).containsEntry("shared", "second"); + } + + @Test + void testBuilderMethodChaining() { + Document document = this.builder.text("chained") + .id("chain-id") + .metadata("key1", "value1") + .metadata("key2", "value2") + .score(0.75) + .build(); + + assertThat(document.getText()).isEqualTo("chained"); + assertThat(document.getId()).isEqualTo("chain-id"); + assertThat(document.getMetadata()).hasSize(2); + assertThat(document.getScore()).isEqualTo(0.75); + } + + @Test + void testTextWithNewlinesAndTabs() { + String textWithFormatting = "Line 1\nLine 2\n\tTabbed line\r\nWindows line ending"; + Document document = this.builder.text(textWithFormatting).build(); + + assertThat(document.getText()).isEqualTo(textWithFormatting); + } + + @Test + void testMetadataOverwritingWithMapAfterKeyValue() { + Map newMetadata = new HashMap<>(); + newMetadata.put("map-key", "map-value"); + + Document document = this.builder.text("test") + .metadata("old-key", "old-value") + .metadata("another-key", "another-value") + .metadata(newMetadata) // This should replace all previous metadata + .build(); + + assertThat(document.getMetadata()).hasSize(1); + assertThat(document.getMetadata()).containsEntry("map-key", "map-value"); + assertThat(document.getMetadata()).doesNotContainKey("old-key"); + assertThat(document.getMetadata()).doesNotContainKey("another-key"); + } + + @Test + void testMetadataKeyValuePairsAccumulation() { + Document document = this.builder.text("test") + .metadata("a", "1") + .metadata("b", "2") + .metadata("c", "3") + .metadata("d", "4") + .metadata("e", "5") + .build(); + + assertThat(document.getMetadata()).hasSize(5); + assertThat(document.getMetadata().keySet()).containsExactlyInAnyOrder("a", "b", "c", "d", "e"); + } + } diff --git a/spring-ai-commons/src/test/java/org/springframework/ai/document/DocumentTests.java b/spring-ai-commons/src/test/java/org/springframework/ai/document/DocumentTests.java index 1845710b617..545ac76bdce 100644 --- a/spring-ai-commons/src/test/java/org/springframework/ai/document/DocumentTests.java +++ b/spring-ai-commons/src/test/java/org/springframework/ai/document/DocumentTests.java @@ -213,4 +213,150 @@ private static Media getMedia() { return Media.builder().mimeType(MimeTypeUtils.IMAGE_JPEG).data(URI.create("http://type1")).build(); } + @Test + void testMetadataModeNone() { + Map metadata = new HashMap<>(); + metadata.put("secret", "hidden"); + + Document document = Document.builder().text("Visible content").metadata(metadata).build(); + + String formattedContent = document.getFormattedContent(MetadataMode.NONE); + assertThat(formattedContent).contains("Visible content"); + assertThat(formattedContent).doesNotContain("secret"); + assertThat(formattedContent).doesNotContain("hidden"); + } + + @Test + void testMetadataModeEmbed() { + Map metadata = new HashMap<>(); + metadata.put("embedKey", "embedValue"); + metadata.put("filterKey", "filterValue"); + + Document document = Document.builder().text("Test content").metadata(metadata).build(); + + String formattedContent = document.getFormattedContent(MetadataMode.EMBED); + // This test assumes EMBED mode includes all metadata - adjust based on actual + // implementation + assertThat(formattedContent).contains("Test content"); + } + + @Test + void testDocumentBuilderChaining() { + Map metadata = new HashMap<>(); + metadata.put("chain", "test"); + + Document document = Document.builder() + .text("Chain test") + .metadata(metadata) + .metadata("additional", "value") + .score(0.85) + .build(); + + assertThat(document.getText()).isEqualTo("Chain test"); + assertThat(document.getMetadata()).containsEntry("chain", "test"); + assertThat(document.getMetadata()).containsEntry("additional", "value"); + assertThat(document.getScore()).isEqualTo(0.85); + } + + @Test + void testDocumentWithScoreGreaterThanOne() { + Document document = Document.builder().text("High score test").score(1.5).build(); + + assertThat(document.getScore()).isEqualTo(1.5); + } + + @Test + void testMutateWithChanges() { + Document original = Document.builder().text("Original text").score(0.5).metadata("original", "value").build(); + + Document mutated = original.mutate().text("Mutated text").score(0.8).metadata("new", "metadata").build(); + + assertThat(mutated.getText()).isEqualTo("Mutated text"); + assertThat(mutated.getScore()).isEqualTo(0.8); + assertThat(mutated.getMetadata()).containsEntry("new", "metadata"); + assertThat(original.getText()).isEqualTo("Original text"); // Original unchanged + } + + @Test + void testDocumentEqualityWithDifferentScores() { + Document doc1 = Document.builder().id("sameId").text("Same text").score(0.5).build(); + + Document doc2 = Document.builder().id("sameId").text("Same text").score(0.8).build(); + + // Assuming score affects equality - adjust if it doesn't + assertThat(doc1).isNotEqualTo(doc2); + } + + @Test + void testDocumentWithComplexMetadata() { + Map nestedMap = new HashMap<>(); + nestedMap.put("nested", "value"); + + Map metadata = new HashMap<>(); + metadata.put("string", "value"); + metadata.put("number", 1); + metadata.put("boolean", true); + metadata.put("map", nestedMap); + + Document document = Document.builder().text("Complex metadata test").metadata(metadata).build(); + + assertThat(document.getMetadata()).containsEntry("string", "value"); + assertThat(document.getMetadata()).containsEntry("number", 1); + assertThat(document.getMetadata()).containsEntry("boolean", true); + assertThat(document.getMetadata()).containsEntry("map", nestedMap); + } + + @Test + void testMetadataImmutability() { + Map originalMetadata = new HashMap<>(); + originalMetadata.put("key", "value"); + + Document document = Document.builder().text("Immutability test").metadata(originalMetadata).build(); + + // Modify original map + originalMetadata.put("key", "modified"); + originalMetadata.put("newKey", "newValue"); + + // Document's metadata should be unaffected (if properly copied) + assertThat(document.getMetadata()).containsEntry("key", "value"); + assertThat(document.getMetadata()).doesNotContainKey("newKey"); + } + + @Test + void testDocumentWithEmptyMetadata() { + Document document = Document.builder().text("Empty metadata test").metadata(new HashMap<>()).build(); + + assertThat(document.getMetadata()).isEmpty(); + } + + @Test + void testMetadataWithNullValueInMap() { + Map metadata = new HashMap<>(); + metadata.put("validKey", "validValue"); + metadata.put("nullKey", null); + + assertThrows(IllegalArgumentException.class, () -> { + Document.builder().text("test").metadata(metadata).build(); + }); + } + + @Test + void testDocumentWithWhitespaceOnlyText() { + String whitespaceText = " \n\t\r "; + Document document = Document.builder().text(whitespaceText).build(); + + assertThat(document.getText()).isEqualTo(whitespaceText); + assertThat(document.isText()).isTrue(); + } + + @Test + void testDocumentHashCodeConsistency() { + Document document = Document.builder().text("Hash test").metadata("key", "value").score(0.1).build(); + + int hashCode1 = document.hashCode(); + int hashCode2 = document.hashCode(); + + assertThat(hashCode1).isEqualTo(hashCode2); + } + }