homelabby/.planning/phases/05-cable-test-integration/05-02-SUMMARY.md
Mikkel Georgsen c9f874b7f4 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
2026-04-10 07:16:50 +00:00

6.5 KiB

phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
05-cable-test-integration 02 netbox-client, api-handlers, router
netbox
cable-test
sse
tdd
handler
requires provides affects
05-01
CreateCable
TestHandler
/api/test/cable
/api/test/events
/api/test/recent
internal/netbox/client.go
internal/api/router.go
cmd/hwlab/main.go
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
created modified
internal/api/handlers/test.go
internal/api/handlers/test_test.go
internal/netbox/client.go
internal/netbox/types.go
internal/netbox/client_test.go
internal/api/router.go
cmd/hwlab/main.go
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
duration_minutes completed_date tasks_completed tasks_total files_created files_modified
25 2026-04-10 2 2 2 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

  • internal/netbox/client.go — CreateCable method exists
  • internal/netbox/types.go — CableRecord type exists
  • internal/netbox/client_test.go — 3 CreateCable unit tests
  • internal/api/handlers/test.go — TestHandler with 3 endpoints
  • internal/api/handlers/test_test.go — 7 test cases
  • internal/api/router.go — NewRouter has testHandler param, 3 routes
  • cmd/hwlab/main.go — testHandler constructed and passed to NewRouter
  • Commit 7908d40 exists (Task 1)
  • Commit 1764c7b exists (Task 2)
  • go build ./... passes
  • go test ./... -race all pass