test(tag): close review-flagged gaps in case-collision coverage (#730)

Two adversarial gaps from PR #733 review:

- Unit: exact-case must win even when its id is NOT the lowest, proving
  exact-case short-circuits before the lowest-id tie-break (a naive
  "lowest id across all CI matches" would pick the wrong row).
- Integration: assert findAllByNameIgnoreCase folds the UPPERCASE
  "GLÜCKWÜNSCHE" — the exact string findOrCreate passes — so the umlaut
  proof matches the resolution path under test, not a lowercase probe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-06 11:07:39 +02:00
parent 80f6468d52
commit 2710f2e233
2 changed files with 17 additions and 1 deletions

View File

@@ -85,7 +85,9 @@ class TagCaseCollisionIntegrationTest {
Tag child = persistTag("glückwünsche", "Glückwünsche/glückwünsche", parent.getId()); Tag child = persistTag("glückwünsche", "Glückwünsche/glückwünsche", parent.getId());
// Proof that real Postgres LOWER() folds the umlaut so both rows match case-insensitively. // Proof that real Postgres LOWER() folds the umlaut so both rows match case-insensitively.
assertThat(tagRepository.findAllByNameIgnoreCase("glückwünsche")).hasSize(2); // Query with the UPPERCASE form findOrCreate actually passes — folding LOWER('GLÜCKWÜNSCHE')
// against LOWER(name) is the exact step under test; a lowercase probe wouldn't exercise it.
assertThat(tagRepository.findAllByNameIgnoreCase("GLÜCKWÜNSCHE")).hasSize(2);
// No exact-case "GLÜCKWÜNSCHE" row exists → resolution falls through to the case-insensitive // No exact-case "GLÜCKWÜNSCHE" row exists → resolution falls through to the case-insensitive
// branch with two candidates and must pick the lowest id deterministically, never throwing. // branch with two candidates and must pick the lowest id deterministically, never throwing.

View File

@@ -65,6 +65,20 @@ class TagServiceTest {
verify(tagRepository, never()).save(any()); verify(tagRepository, never()).save(any());
} }
@Test
void findOrCreate_exactCaseWins_evenWhenItsIdIsNotTheLowest() {
// Adversarial guard: exact-case must short-circuit BEFORE the lowest-id rule. Here the exact row
// has the higher id, so a naive "always pick lowest id across all CI matches" would pick wrong.
Tag exactHigherId = Tag.builder().id(UUID.fromString("00000000-0000-0000-0000-000000000009")).name("geburt").build();
when(tagRepository.findByName("geburt")).thenReturn(Optional.of(exactHigherId));
Tag result = tagService.findOrCreate("geburt");
assertThat(result).isEqualTo(exactHigherId);
verify(tagRepository, never()).findAllByNameIgnoreCase(any()); // exact-case wins without consulting the CI list
verify(tagRepository, never()).save(any());
}
@Test @Test
void findOrCreate_usesSingleCaseInsensitiveMatch_whenNoExactCase() { void findOrCreate_usesSingleCaseInsensitiveMatch_whenNoExactCase() {
// Stored name is "Weihnachten"; a save replays "weihnachten" (no exact-case row) → bind to the // Stored name is "Weihnachten"; a save replays "weihnachten" (no exact-case row) → bind to the