Compare commits

..

8 Commits

Author SHA1 Message Date
Marcel
fc2c726961 test(tag): explicitly stub the subtree rollup query in getTagTree tests (#698)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m20s
CI / OCR Service Tests (pull_request) Successful in 19s
CI / Backend Unit Tests (pull_request) Successful in 3m26s
CI / fail2ban Regex (pull_request) Successful in 43s
CI / Semgrep Security Scan (pull_request) Successful in 18s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m2s
Address review nit: the older getTagTree tests relied on Mockito's default
empty-list return for findSubtreeDocumentCountsPerTag. Stub it explicitly so
the two-query contract is self-documenting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:40:54 +02:00
Marcel
98be732b05 test(admin-tags): pin merge/delete previews to the direct count (#698)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m15s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m24s
CI / fail2ban Regex (pull_request) Successful in 43s
CI / Semgrep Security Scan (pull_request) Successful in 19s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
Characterization tests for AC#8: the merge preview and the delete-impact
warning describe direct-document operations, so they must report the tag's
direct documentCount, never a subtree rollup. Both tests pass a stray
subtreeDocumentCount and assert it does not leak into the preview, so a future
change can't silently desync a destructive-action preview.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:26:42 +02:00
Marcel
aa9d4aafaa test(tag): add subtreeDocumentCount to admin tree fixtures (#698)
TagTreeNodeDTO now requires subtreeDocumentCount, so the admin sidebar test
fixtures (TagTreeNode, TagsListPanel) need the field to type-check. The admin
sidebar still renders the direct documentCount — these fixtures only gain the
new field, no behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:26:18 +02:00
Marcel
1fd8c2b2d2 feat(themen): show the subtree rollup count on reader surfaces (#698)
The /themen page (box header, child rows, aria-labels) and the dashboard
ThemenWidget now display subtreeDocumentCount instead of the direct
documentCount, so a topic's number reflects its whole sub-topic tree and
matches what /documents?tag=X actually returns. A parent with 0 direct
documents but documents under its children now shows a non-zero total.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:25:52 +02:00
Marcel
e8377b579e feat(themen): key reader tag visibility on the subtree rollup (#698)
Regenerate the TagTreeNodeDTO type with subtreeDocumentCount and switch
hasAnyDocuments to read it directly — the backend rollup already includes all
descendants, so the recursive children walk is no longer needed. Reader
surfaces now hide a topic only when its whole subtree is empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:17:40 +02:00
Marcel
4e8962a06d docs(tag): document the dual document counts on the tag tree (#698)
Record that getTagTree returns both documentCount (direct, read by admin
surfaces) and subtreeDocumentCount (rollup, read by the reader surfaces),
matching the corrected getTagTree JavaDoc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:15:52 +02:00
Marcel
08a7f8920c test(tag): validate subtree rollup CTE against real Postgres (#698)
Cover AC#1-4 (leaf=direct, distinct overlap counted once, full descendant
depth), REQ-THEMEN-05 (empty subtree absent), REQ-THEMEN-06 (cycle terminates
via the 50-level guard) and AC#7 (rollup equals distinct documents found by the
real tag-search expansion — count↔destination parity). Testcontainers
postgres:16-alpine since the recursive CTE + COUNT(DISTINCT) needs real PG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:15:15 +02:00
Marcel
335ef18a8e feat(tag): add subtree document-count rollup to tag tree (#698)
Add subtreeDocumentCount to TagTreeNodeDTO, populated by a new recursive-CTE
aggregate query that builds a tag closure and counts distinct documents per
ancestor subtree. The direct documentCount is unchanged; getTagTree now maps
both counts onto each node from two aggregate queries (no N+1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:12:33 +02:00
3 changed files with 1 additions and 34 deletions

View File

@@ -297,13 +297,6 @@ class DocumentControllerTest {
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(authorities = "READ_ALL")
void createDocument_returns403_forReaderOnly() throws Exception {
mockMvc.perform(multipart("/api/documents").with(csrf()))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(authorities = "WRITE_ALL")
void createDocument_returns200_whenHasWritePermission() throws Exception {
@@ -421,13 +414,6 @@ class DocumentControllerTest {
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(authorities = "READ_ALL")
void quickUpload_returns403_forReaderOnly() throws Exception {
mockMvc.perform(multipart("/api/documents/quick-upload").with(csrf()))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(authorities = "WRITE_ALL")
void quickUpload_returns200_withValidPdfFile() throws Exception {

View File

@@ -43,8 +43,6 @@ const isAdmin = $derived(
data?.user?.groups?.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN'))
);
const canUpload = $derived(Boolean(data?.user && data.canWrite));
// Set after client-side hydration completes. Used by E2E tests to know the
// page is interactive (event handlers registered) before they interact with it.
let hydrated = $state(false);
@@ -77,7 +75,7 @@ const userInitials = $derived.by(() => {
<!-- Right Side -->
<div class="flex items-center gap-3">
{#if canUpload}
{#if data?.user}
<a
href="/documents/new"
aria-label={m.upload_action()}

View File

@@ -83,23 +83,6 @@ describe('Layout upload link', () => {
const link = page.getByRole('link', { name: /Hochladen|Upload|Subir/i });
await expect.element(link).toHaveAttribute('href', '/documents/new');
});
it('is hidden for a user without WRITE_ALL', async () => {
render(Layout, { data: makeData({ canWrite: false }), children: emptySnippet });
await expect
.element(page.getByRole('link', { name: /Hochladen|Upload|Subir/i }))
.not.toBeInTheDocument();
});
it('is hidden for an ANNOTATE_ALL-only user (gate is lack of WRITE_ALL, not READ_ALL)', async () => {
render(Layout, {
data: makeData({ canWrite: false, canAnnotate: true }),
children: emptySnippet
});
await expect
.element(page.getByRole('link', { name: /Hochladen|Upload|Subir/i }))
.not.toBeInTheDocument();
});
});
// ─── Dropdown ─────────────────────────────────────────────────────────────────