feat(graph): phase 3 graph view — Slice 2 (JSON endpoint) - #582
Conversation
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Adds 5 unit tests for the /api/graph.json handler covering missing entity, invalid DN, unknown DN, ETag stability with If-None-Match, and depth clamping for out-of-range values. To make Manager seedable from outside ldap_cache, exposes three ForTesting hooks (SetUsers/Groups/ComputersForTesting) and a new internal/ldap_cache/cachetest package with reflection-based constructors that populate simple-ldap-go's unexported dn/cn fields. Registers /api/graph.json in setupFullTestApp without RequireAuth, mirroring the existing pattern for /api/search-index.json — the handler only reads from app.ldapCache and needs no session state. Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
There was a problem hiding this comment.
Automated approval for maintainer PR
All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #582 +/- ##
==========================================
+ Coverage 68.07% 68.23% +0.15%
==========================================
Files 29 32 +3
Lines 3092 3167 +75
==========================================
+ Hits 2105 2161 +56
- Misses 836 854 +18
- Partials 151 152 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds the Phase 3 relationship graph JSON API endpoint (GET /api/graph.json) in the protected web server routes, plus supporting test infrastructure to seed the in-memory LDAP cache from internal/web tests.
Changes:
- Register
/api/graph.jsonin the protected route group and in test harnesses. - Implement
handleGraphJSONwith query validation, ETag-based conditional GET, and graph building vialdap_cache.Manager.BuildGraph. - Add unit/integration tests and test-only cache seeding helpers (
cachetest+Set*ForTesting).
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/web/server.go | Registers the new protected /api/graph.json route. |
| internal/web/server_test.go | Exposes /api/graph.json without auth in the test harness (mirrors search-index test approach). |
| internal/web/ldap_integration_test.go | Registers /api/graph.json on the integration-test app’s protected router. |
| internal/web/graph_v2_handler.go | Implements the JSON endpoint, validation, ETag caching, and BuildGraph invocation/logging. |
| internal/web/graph_v2_handler_test.go | Adds unit tests for validation, ETag stability, and depth clamping behavior. |
| internal/web/graph_integration_test.go | Adds an LDAP-backed integration test for the endpoint behind auth. |
| internal/ldap_cache/seed_for_tests.go | Adds Manager.Set*ForTesting hooks to allow seeding caches without LDAP I/O. |
| internal/ldap_cache/cachetest/helpers.go | Adds cross-package test helpers to construct LDAP objects and seed the manager. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- handleGraphJSON: on json.Marshal failure, log via zerolog with the entity from the query string and return a plain "internal error" (500) instead of propagating to Fiber's HTML ErrorHandler. Mirrors the BuildGraph error path so the JSON API endpoint stays consistent. - handleGraphJSON: per RFC 7232 §4.1, set ETag and Cache-Control before the If-None-Match check so 304 responses also carry the validators clients/proxies need to refresh caches. Content-Type is now set only on the 200 path (304 has no body). - cachetest helpers: add per-occurrence justification on the two unsafe.Pointer //nolint:gosec calls so GitHub Advanced Security has the required reason inline. Hoisted the unsafe.Pointer computations onto separate lines to keep within the project's lll line-length limit. - Added TestHandleGraphJSON_NotModifiedKeepsHeaders to lock in the RFC 7232 behaviour as a regression guard. Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
There was a problem hiding this comment.
Automated approval for maintainer PR
All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.
| dnField := obj.FieldByName("dn") | ||
| cnField := obj.FieldByName("cn") | ||
| // test-only writers for unexported simple-ldap-go fields; never used in production code. | ||
| dnPtr := unsafe.Pointer(dnField.UnsafeAddr()) //nolint:gosec // test-only DN field write |
| cnField := obj.FieldByName("cn") | ||
| // test-only writers for unexported simple-ldap-go fields; never used in production code. | ||
| dnPtr := unsafe.Pointer(dnField.UnsafeAddr()) //nolint:gosec // test-only DN field write | ||
| cnPtr := unsafe.Pointer(cnField.UnsafeAddr()) //nolint:gosec // test-only CN field write |
…+ CSS) (#584) ## Summary Slice 3 of 6 for the Phase 3 relationship graph view. Adds the server-side-rendered HTML page that consumes the JSON endpoint from Slice 2. Plan: `docs/superpowers/plans/2026-04-24-phase-3-graph-view.md` Tasks 15-22. Slice 1 (in-memory builder) is on main as #581; Slice 2 (JSON endpoint) is on main as #582. ## What ships - **`GET /graph?entity=<dn>&depth=<N>`** — HTML page rendering the same graph data as `/api/graph.json` via the templ `GraphPageV2` template. - **No-JS fallback path** is the primary deliverable: depth slider has `<noscript>` Apply button, SVG nodes render at SSR-computed positions, edge table is fully functional. Slice 4 will add interactivity on top. - **WCAG 2.2 compliance baseline:** - SSR edge table mirrors the visual graph (AAA-mandated text alternative). - SVG `role="img"` + `aria-labelledby`/`aria-describedby`; nodes get `role="button"` + `tabindex="0"` + descriptive `aria-label`. - `prefers-reduced-motion: reduce` honored. - All graph color tokens hit AAA (7:1) for text and AA (3:1) for non-text per §1.4.11. ## Files - `internal/web/templates/graph_v2.templ` — full SSR template + helpers - `internal/web/graph_v2_handler.go` — `handleGraphV2` HTML handler + focus label/type helpers - `internal/web/server.go` — `/graph` route in protected group - `internal/web/static/app.css` — graph styles + light/dark tokens + motion-reduce override - `internal/web/contrast_test.go` — 6 new contrast pair assertions, fixed latent dark-block parser bug ## Tests - `TestHandleGraphV2_RendersHTML` — verifies status 200 + 4 HTML markers (`graph-canvas`, `graph-data`, `graph-table`, `Relationships: bob`). - `TestAppCSSContrastAAA` — extended with 6 graph pairs (3 light, 3 dark) covering edge/edge-focus/node-border. Latent parser bug fixed (`FindStringSubmatch` → `FindAllStringSubmatch` for dark blocks). - All Slice 1+2 tests still pass (`TestHandleGraphJSON_*`, `TestBuildGraph_*`). ## Notable design decisions - **Inline JSON via `@templ.Raw(graphInlineScript(data))`** — templ v0.3 treats `<script>` contents as opaque text, so the script element is built in Go and emitted as raw HTML outside any script block. JSON is `<`/`>`/`&`-escaped to Unicode by `json.Marshal`, so DN values cannot break out of the script tag. - **Concentric ring multiplier 150** (was 180 in plan) — keeps ring-3 nodes inside the SVG viewBox. - **`GraphPageVM.Sort*` and `BackHref` fields reserved** for Slice 5 list-page mode wiring. - **`/graph` 400/404 returns plain text** — consistent with `/api/graph.json`. Slice 5 may add a styled HTML error page. ## Test plan - [x] `go test ./internal/web/ -count=1` → ok - [x] `go test ./internal/ldap_cache/...` → no regressions - [x] `golangci-lint run ./...` → 0 issues - [x] `go build ./...` → clean - [x] `templ generate` → produces `graph_v2_templ.go` (45 KB), gitignored - [ ] CI verification on push - [ ] Visual smoke (after merge): `docker compose --profile dev up -d` then browse to `/graph?entity=<some-DN>` to confirm SSR-only rendering looks reasonable ## What's next - **Slice 4:** `static/js/v2-graph.js` — pan/zoom, keyboard navigation, click-to-pivot, depth-slider on input, edge highlighting, ARIA live announcements. - **Slice 5+6:** list-page Graph mode, drawer pivots, AAA parallel-list verification, docs.
## Summary Slice 4 of 6 for the Phase 3 relationship graph view. Adds the client-side JS layer that turns Slice 3's SSR view into an interactive canvas (pan/zoom/keyboard nav/click-to-expand) without breaking the no-JS fallback. Plan: `docs/superpowers/plans/2026-04-24-phase-3-graph-view.md` Tasks 23-29. Slices 1-3 already on main as #581 / #582 / #584. ## What ships - **`internal/web/static/js/v2-graph.js`** (~290 lines, IIFE, plain JS, CSP-safe) — pan/zoom (mouse drag, wheel+ctrl/meta, arrow keys, +/-), keyboard nav (Tab/Shift-Tab cycles nodes), click-to-pivot (node body → drawer route), click-to-expand (`+` badge → fetch `/api/graph.json?depth=1`, merge nodes/edges, announce via aria-live), depth slider auto-submit on change. - **Activates SVG nodes for interactivity** at runtime (re-adds `tabindex`, `role="button"`, "Press Enter to open" suffix that Slice 3 deliberately dropped because there was no JS handler then). - **Reads inline graph data from Slice 3's `<template id="graph-data">`** via `template.content.textContent` — works under strict `script-src 'self'` CSP. - **Renders dynamically added expandable nodes with their `+` badge** so they're discoverable with the mouse, not just the keyboard. - **Handles fetch errors gracefully** — bad responses get logged + announced "Expand failed." rather than silently no-op. - Script wired in `base_v2.templ` head with `defer`; bails immediately on pages without `#graph-data`. - **`internal/e2e/graph_test.go`** — Playwright happy-path: login → /users → grab DN → /graph?entity=…&depth=2 → assert SVG + table + parseable inline data → click expandable badge → assert aria-live announces → reduced-motion repeat. ## Notable design decisions - **No build step, no transpilation, no module system** — matches `v2-bulk.js` / `v2-drawer.js` precedent. IIFE wrapper, `'use strict'`, `var` (not `let`/`const`), `function ()` callbacks (no arrows). - **Ring multiplier 150** in `renderNode`/`renderEdge` matches Slice 3's SSR `concentricXY` so dynamically-added nodes line up with SSR-positioned ones. - **`CSS.escape(dn)` before selector use** — DNs contain commas/equals that would break attribute selectors. - **`fetch` with `credentials: 'same-origin'`** — required for the session-authed endpoint. - **Reduced-motion: nothing extra in JS** — the CSS already disables transitions; JS doesn't introduce new ones. ## Tests - Existing Slice 1-3 unit tests still pass. - `TestGraphHappyPath` (Playwright, behind `//go:build e2e`) — runs in CI. ## Test plan - [x] `go build ./...` — clean - [x] `go build -tags e2e ./internal/e2e/...` — clean - [x] `go test ./internal/web/ ./internal/ldap_cache/...` — pass, no regressions - [x] `golangci-lint run ./...` — 0 issues - [x] `node -c internal/web/static/js/v2-graph.js` — clean syntax - [ ] CI verification on push (unit + e2e) - [ ] Visual smoke (after merge): browse to `/graph?entity=<DN>` and try drag, ctrl+wheel zoom, Tab between nodes, click `+` badge to expand ## What's next - **Slice 5:** list-page Graph mode (`/users?view=graph`, `/groups?view=graph`, `/computers?view=graph`) — Tasks 30-34. - **Slice 6:** drawer "View relationships" pivot, axe-core E2E ratchet, README conformance statement — Tasks 35-40.
…t + docs) (#587) ## Summary Final slice of the Phase 3 relationship graph view. Adds the drawer entry point, the axe-core a11y ratchet, and the README conformance statement. Plan: `docs/superpowers/plans/2026-04-24-phase-3-graph-view.md` Tasks 35-40. Slices 1-5 already on main (#581 / #582 / #584 / #585 / #586). ## What ships - **"View relationships" drawer pivot** on user, group, and computer drawers (Tasks 35-36). Inserted as the second pivot after "Open full page", with a new `iconGraph` SVG (4 nodes connected as a square). Links to `/graph?entity=<DN>`. - **`internal/e2e/axe_graph_test.go`** (Task 37) — runs axe-core against `/graph?entity=<seeded-DN>&depth=2`, `/users?view=graph`, `/groups?view=graph`, `/computers?view=graph`. Uses the WCAG 2.2 AA ruleset (graph view's stated conformance level — the AAA login ratchet stays in place separately). Each subtest waits for `svg#graph-canvas` so axe sees the JS-mutated DOM. - **README accessibility section updated** (Task 39) — adds a sentence about the graph view's WCAG 2.2 AA conformance and the AAA-equivalent flat edge table fallback. ## Deferred - **Task 38 (tab-order snapshot):** plan called for adding graph pages to a "tab-order snapshot suite" — but no such suite exists in the codebase. Building one from scratch for just three pages is a separate a11y-tooling effort. Deferred — file as a future enhancement. ## Tests - All Slice 1-5 tests still pass. - New axe-core ratchet runs in CI under the `e2e` build tag. ## Test plan - [x] `go build ./...` — clean - [x] `go build -tags e2e ./internal/e2e/...` — clean - [x] `go test ./... -count=1` — all packages pass - [x] `golangci-lint run ./...` — 0 issues - [ ] CI verification on push (unit + integration + e2e) - [ ] Manual smoke (after merge): open a user drawer → click "View relationships" → confirm graph renders → use the `List | Graph` toggle to switch back ## Phase 3 complete This PR closes out the Phase 3 relationship graph view per spec `docs/superpowers/specs/2026-04-24-phase-3-graph-view-design.md`. With this merged: - `BuildGraph(focusDN, depth)` + `BuildListGraph(users, computers)` in-memory builders (Slice 1, Slice 5) - `/api/graph.json?entity=…&depth=…` JSON endpoint with ETag/304/RFC 7232 (Slice 2) - `/graph` HTML page with SSR SVG + edge table (Slice 3) - `v2-graph.js` interactive client: pan/zoom, keyboard nav, click-to-pivot, click-to-expand, depth slider auto-submit, ARIA live announce (Slice 4) - `?view=graph` list-page mode for users/groups/computers with `List | Graph` segmented toggle (Slice 5) - "View relationships" drawer pivot + axe-core ratchet + README conformance (this PR)
Summary
Slice 2 of 6 for the Phase 3 relationship graph view. Adds the JSON endpoint that the upcoming HTML/SVG slices will consume.
Plan:
docs/superpowers/plans/2026-04-24-phase-3-graph-view.mdTasks 10-14. Slice 1 (the in-memory graph builder) is already on main as #581.What ships
GET /api/graph.json?entity=<dn>&depth=<N>— registered inprotectedgroup ofinternal/web/server.go./api/search-index.jsonpattern);If-None-Matchshort-circuits to 304;Cache-Control: private, must-revalidate.BuildGraph."internal error"; underlying error logged via zerolog withentity+depthcontext (no DN/internals leakage).Test infrastructure changes
To let
internal/web/tests seedinternal/ldap_cache.Managerfrom outside the package:internal/ldap_cache/cachetest/sub-package — exportsNewUserWithDN,NewGroupWithDN,NewComputerWithDN,Seed. Uses reflection+unsafe to populate the unexportedObject.dn/cnfields onsimple-ldap-gotypes. Test-only by convention; not imported by any production code.internal/ldap_cache/seed_for_tests.go— addsManager.SetUsersForTesting / SetGroupsForTesting / SetComputersForTestingexported methods. Thin wrappers around the unexportedsetAll.ForTestingsuffix per Go convention.Tests (7 total, all green)
TestHandleGraphJSON_MissingEntity— 400 on missing?entity=.TestHandleGraphJSON_InvalidDN— 400 on garbage DN.TestHandleGraphJSON_UnknownDN— 404 when DN not in cache.TestHandleGraphJSON_InvalidDepth— 400 on non-numeric depth.TestHandleGraphJSON_ETagStable— first call returns 200+ETag, second withIf-None-Matchreturns 304.TestHandleGraphJSON_DepthClamping— depth=0/99/-5 all return 200 withDepth ∈ [1,3].TestGraphJSON_IntegrationUserFocus— exercises the full stack against real OpenLDAP (skipped if no container at127.0.0.1:1389).Test plan
go test ./internal/web/ -run TestHandleGraphJSON -count=1 -v→ 6 unit tests PASSosixia/openldap:1.5.0withdc=test,dc=localgo test ./internal/ldap_cache/... -count=1→ no Slice 1 regressiongolangci-lint run ./internal/web/... ./internal/ldap_cache/...→ 0 issuesgo build ./...cleanWhat's next
Slice 3: HTML template + SSR-rendered SVG (no-JS fallback path). Slice 4: interactive canvas. Slices 5-6: list-page mode + drawer pivots + AAA parallel list view + docs.