refactor(#248): address PR review concerns — TagOperator enum, typed projection, bean validation

- Replace stringly-typed "AND"/"OR" tagOperator with TagOperator enum (DocumentService, DocumentController)
- Replace Object[] with TagCount projection interface in TagRepository.findDocumentCountsPerTag()
- Use @NotNull + @Valid on MergeTagDTO.targetId; remove manual null check from TagController
- Correct ALLOWED_TAG_COLORS to match actual frontend CSS tokens (sage/sienna/amber/slate/violet/rose/cobalt/moss/sand/coral)
- Add TOCTOU comment to validateNoAncestorCycle() with mitigation explanation
- Add test: deleteWithDescendants_skipsDocTagDeletion_whenDescendantIdsIsEmpty
- Update TagServiceTest to use mock TagRepository.TagCount projection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-17 00:24:04 +02:00
parent 172c5613ed
commit d7a46de1cc
8 changed files with 58 additions and 18 deletions

View File

@@ -18,6 +18,7 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@@ -256,8 +257,11 @@ class TagServiceTest {
void getTagTree_populatesDocumentCount_fromAggregateQuery() {
UUID tagId = UUID.randomUUID();
Tag tag = Tag.builder().id(tagId).name("Tag").build();
TagRepository.TagCount countEntry = mock(TagRepository.TagCount.class);
when(countEntry.getTagId()).thenReturn(tagId);
when(countEntry.getCount()).thenReturn(5L);
when(tagRepository.findAll()).thenReturn(List.of(tag));
when(tagRepository.findDocumentCountsPerTag()).thenReturn(List.<Object[]>of(new Object[]{tagId, 5L}));
when(tagRepository.findDocumentCountsPerTag()).thenReturn(List.of(countEntry));
var tree = tagService.getTagTree();
@@ -456,6 +460,19 @@ class TagServiceTest {
verify(tagRepository).deleteAllById(List.of(id));
}
@Test
void deleteWithDescendants_skipsDocTagDeletion_whenDescendantIdsIsEmpty() {
UUID id = UUID.randomUUID();
Tag tag = Tag.builder().id(id).name("Tag").build();
when(tagRepository.findById(id)).thenReturn(Optional.of(tag));
when(tagRepository.findDescendantIds(id)).thenReturn(List.of());
tagService.deleteWithDescendants(id);
verify(tagRepository, never()).deleteDocumentTagsByTagIds(any());
verify(tagRepository).deleteAllById(List.of());
}
// ─── delete ───────────────────────────────────────────────────────────────
@Test