docs(05-02): complete NetBox CreateCable + TestHandler plan

- SUMMARY.md: 2 tasks, 2 commits, threat mitigations T-05-03/04/05 applied
- Deviation: go-netbox Cable model required url/display fields in test mock
This commit is contained in:
Mikkel Georgsen 2026-04-10 07:16:50 +00:00
parent 1764c7b06a
commit c9f874b7f4

View file

@ -0,0 +1,123 @@
---
phase: 05-cable-test-integration
plan: "02"
subsystem: netbox-client, api-handlers, router
tags: [netbox, cable-test, sse, tdd, handler]
dependency_graph:
requires: ["05-01"]
provides: ["CreateCable", "TestHandler", "/api/test/cable", "/api/test/events", "/api/test/recent"]
affects: ["internal/netbox/client.go", "internal/api/router.go", "cmd/hwlab/main.go"]
tech_stack:
added: []
patterns:
- "TestNetBoxClient interface for handler injection (mirrors IntakeNetBoxClient pattern)"
- "TestPrinter interface for nil-safe auto-print with 1s rate limit (mirrors LabelHandler)"
- "SSE handler mirrors usb_events.go: 30s keepalive, r.Context().Done() exit"
- "Ring buffer capped at 20 items, prepend-newest, sync.Mutex protected"
- "httptest.NewUnstartedServer for mock responses needing srv.URL in handler"
key_files:
created:
- internal/api/handlers/test.go
- internal/api/handlers/test_test.go
modified:
- internal/netbox/client.go
- internal/netbox/types.go
- internal/netbox/client_test.go
- internal/api/router.go
- cmd/hwlab/main.go
decisions:
- "Used DisallowUnknownFields on JSON decoder for POST /api/test/cable (T-05-03 mitigation)"
- "Print rate limit 1s inherits LabelHandler pattern (T-05-05 mitigation)"
- "liveCh is a buffered channel (cap 64) owned by TestHandler; AttachStream fans in from driver channels"
- "deriveCableLabel builds label from CableType + version string — cable names are descriptive not unique"
- "httptest.NewUnstartedServer used in cable test mock so srv.URL is capturable in handler closure"
metrics:
duration_minutes: 25
completed_date: "2026-04-10"
tasks_completed: 2
tasks_total: 2
files_created: 2
files_modified: 5
---
# Phase 05 Plan 02: NetBox CreateCable + TestHandler + 3 Endpoints Summary
**One-liner:** NetBox cable creation via `DcimCablesCreate` with `test_data`/`catalog_status` custom fields, plus `TestHandler` providing SSE live readings, 20-item ring buffer, and auto-print with 1s rate limit on three `/api/test/*` routes.
## Tasks Completed
| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | NetBox CreateCable method + CableRecord type | `7908d40` | internal/netbox/client.go, types.go, client_test.go |
| 2 | TestHandler (3 endpoints) + router wiring + main.go init | `1764c7b` | handlers/test.go, test_test.go, router.go, main.go |
## What Was Built
### Task 1: CreateCable + CableRecord
- `CableRecord` type added to `internal/netbox/types.go` with ID, HWID, Label, TestData (raw JSON), CatalogStatus fields
- `CreateCable(ctx, label, assetTag, testDataJSON string) (int64, error)` on `*netbox.Client`
- Uses `nb.NewWritableCableRequest()``DcimCablesCreate` (mirrors `CreateDevice` pattern)
- Sets `test_data` and `catalog_status: "complete"` custom fields; `hw_id` if assetTag non-empty
- Rejects empty label with sentinel error `"cable label must not be empty"`
- Unit tests: empty label rejection, 201 success (httptest mock with required `url`/`display` fields), 422 error
### Task 2: TestHandler + Router + main.go
- `TestHandler` struct with `TestNetBoxClient` and `TestPrinter` interfaces for injection
- `POST /api/test/cable`: decodes JSON with `DisallowUnknownFields` (T-05-03), derives label via `deriveCableLabel`, calls `CreateCable`, attempts auto-print with 1s cooldown (T-05-05), prepends to 20-item ring buffer, returns `{hw_id, netbox_id, print_skipped}`
- `GET /api/test/events`: SSE with `Content-Type: text/event-stream`, 30s keepalive ticker, exits on `r.Context().Done()` (T-05-04 goroutine-leak-safe)
- `GET /api/test/recent`: mutex-locked copy of ring buffer, returns `[]` when empty
- `AttachStream(ch <-chan tester.LiveReading)`: fans in from driver channel to internal buffered channel
- Router: `NewRouter` signature extended with `*handlers.TestHandler`, three routes registered
- main.go: `testHandler` constructed with `nbClient` + `mockDriver`; USB Manager goroutine wired (stub logs `RoleCableTester` connect events)
## Verification
```
go build ./... PASS
go test ./internal/netbox/... -run TestCreateCable -v 3/3 PASS
go test ./internal/api/handlers/... -race -v 7/7 PASS (TestTestHandler_*)
go test ./... -race All packages PASS
```
## Threat Mitigations Applied
| Threat ID | Mitigation | Implementation |
|-----------|-----------|----------------|
| T-05-03 | Tampering — JSON body | `json.NewDecoder(...).DisallowUnknownFields()` on POST /api/test/cable |
| T-05-04 | DoS — SSE goroutine leak | `select` on `r.Context().Done()` in `StreamEvents` |
| T-05-05 | DoS — runaway print calls | 1s `printCooldown` in `TestHandler`, same pattern as `LabelHandler` |
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] go-netbox Cable response model requires `url` and `display` fields**
- **Found during:** Task 1, GREEN phase — `TestCreateCable_Success` failed with "no value given for required property url"
- **Issue:** The `Cable` response model (generated from NetBox OpenAPI spec) validates required properties `id`, `url`, `display` during JSON unmarshal. Mock returning only `{id, label}` triggered validation error.
- **Fix:** Updated mock to return `url` and `display` fields; switched to `httptest.NewUnstartedServer` so `srv.URL` is capturable for a realistic URL value in the response.
- **Files modified:** `internal/netbox/client_test.go`
- **Commit:** `7908d40`
## Known Stubs
- `main.go` USB Manager goroutine for `RoleCableTester`: logs connection event, does NOT construct a real tester driver or call `AttachStream`. Comment marks the `TODO(hardware)` for Phase 5 hardware integration. This does not prevent the plan's goal — `AttachStream` is wired and tested; hardware arrival is the blocker.
## Threat Flags
None — no new network endpoints, auth paths, or file access patterns beyond those specified in the plan's threat model.
## Self-Check: PASSED
- [x] `internal/netbox/client.go` — CreateCable method exists
- [x] `internal/netbox/types.go` — CableRecord type exists
- [x] `internal/netbox/client_test.go` — 3 CreateCable unit tests
- [x] `internal/api/handlers/test.go` — TestHandler with 3 endpoints
- [x] `internal/api/handlers/test_test.go` — 7 test cases
- [x] `internal/api/router.go` — NewRouter has testHandler param, 3 routes
- [x] `cmd/hwlab/main.go` — testHandler constructed and passed to NewRouter
- [x] Commit `7908d40` exists (Task 1)
- [x] Commit `1764c7b` exists (Task 2)
- [x] `go build ./...` passes
- [x] `go test ./... -race` all pass