--- phase: 02-backend-refactor plan: "02" subsystem: http-handlers tags: [server-struct, dependency-injection, store-interface, test-isolation, in-memory-sqlite, refactor] # Dependency graph requires: - phase: 02-01 provides: Store interface (9 methods), SQLiteStore, RunMigrations provides: - Server struct with Store field and webhookSecret field - NewServer constructor wiring Store and secret - All 6 handlers converted to *Server methods calling s.store.X() - NewTestServer / NewTestServerWithSecret helpers for isolated per-test databases - main.go wiring: sql.Open -> RunMigrations -> NewSQLiteStore -> NewServer -> routes affects: - 03-postgresql (PostgreSQLStore will implement same Store interface; Server struct accepts any Store) # Tech tracking tech-stack: added: [] patterns: - Server struct pattern - all handler dependencies injected via constructor, no package-level globals - export_test.go internal helpers (TestUpsertEvent, TestGetUpdatesMap) - access unexported fields without exposing Store accessor - Per-test in-memory SQLite database via NewTestServer() - eliminates shared state between tests - NewTestServerWithSecret for auth-enabled test scenarios key-files: created: [] modified: - pkg/diunwebhook/diunwebhook.go - pkg/diunwebhook/export_test.go - pkg/diunwebhook/diunwebhook_test.go - cmd/diunwebhook/main.go key-decisions: - "Option B for test store access: internal helpers in export_test.go (TestUpsertEvent, TestGetUpdatesMap) instead of exported Store() accessor - keeps store field unexported" - "t.Errorf used inside goroutines in TestConcurrentUpdateEvent (t.Fatalf is not safe from non-test goroutines)" - "_ modernc.org/sqlite blank import moved from diunwebhook.go to main.go and migrate.go - driver registration happens where needed" patterns-established: - "Server struct: HTTP handlers as methods on *Server, all deps injected at construction" - "NewTestServer pattern: each test creates its own in-memory SQLite DB via RunMigrations + NewSQLiteStore + NewServer" - "export_test.go internal methods: (s *Server) TestUpsertEvent / TestGetUpdatesMap access s.store without exporting Store field" requirements-completed: [REFAC-01, REFAC-02, REFAC-03] # Metrics duration: 3min completed: "2026-03-23" --- # Phase 02 Plan 02: Server Struct Refactor and Test Isolation Summary **Server struct with Store injection, globals removed, all 6 handlers as *Server methods calling s.store.X(), per-test in-memory databases via NewTestServer** ## Performance - **Duration:** ~3 min - **Started:** 2026-03-23T21:02:53Z - **Completed:** 2026-03-23T21:05:09Z - **Tasks:** 2 - **Files modified:** 4 ## Accomplishments - Removed all package-level globals (db, mu, webhookSecret) from diunwebhook.go - Removed InitDB, SetWebhookSecret, UpdateEvent, GetUpdates functions (replaced by Store and Server) - Added Server struct with store Store and webhookSecret string fields - Added NewServer(store Store, webhookSecret string) *Server constructor - Converted all 6 handler functions to *Server methods using s.store.X() for all persistence - Rewrote export_test.go: NewTestServer, NewTestServerWithSecret, TestUpsertEvent, TestGetUpdatesMap helpers - Rewrote diunwebhook_test.go: every test creates its own isolated in-memory database (no shared global state) - Updated main.go: sql.Open -> RunMigrations -> NewSQLiteStore -> NewServer -> route registration - All 35 tests pass against the new Server/Store architecture ## Task Commits Each task was committed atomically: 1. **Task 1: Convert diunwebhook.go to Server struct and update main.go** - `78543d7` (feat) 2. **Task 2: Rewrite export_test.go and update all tests for Server/Store** - `e35b4f8` (test) ## Files Created/Modified - `pkg/diunwebhook/diunwebhook.go` - Server struct, NewServer constructor, all 6 handlers as *Server methods; globals and standalone functions removed - `pkg/diunwebhook/export_test.go` - NewTestServer, NewTestServerWithSecret, (s *Server) TestUpsertEvent, TestGetUpdates, TestGetUpdatesMap - `pkg/diunwebhook/diunwebhook_test.go` - All 35 tests rewritten to use NewTestServer per-test; no shared state; no TestMain - `cmd/diunwebhook/main.go` - Full replacement: sql.Open -> RunMigrations -> NewSQLiteStore -> NewServer -> route registration with srv.XHandler ## Decisions Made - Test store access via internal helper methods in export_test.go (Option B) — avoids exposing Store field publicly while still letting tests call UpsertEvent/GetUpdates - t.Errorf used inside goroutine in TestConcurrentUpdateEvent — t.Fatalf is not safe from non-test goroutines (pre-existing issue resolved) - _ "modernc.org/sqlite" blank import moved to main.go (and already in migrate.go) — driver registered where *sql.DB is opened ## Deviations from Plan None - plan executed exactly as written. ## Known Stubs None. ## Self-Check: PASSED - pkg/diunwebhook/diunwebhook.go: FOUND - pkg/diunwebhook/export_test.go: FOUND - pkg/diunwebhook/diunwebhook_test.go: FOUND - cmd/diunwebhook/main.go: FOUND - Commit 78543d7: FOUND - Commit e35b4f8: FOUND - All 35 tests pass: VERIFIED (go test -v -count=1 ./pkg/diunwebhook/) ## Next Phase Readiness - Server struct accepts any Store implementation — PostgreSQL store can be introduced in Phase 3 without touching handlers - RunMigrations called in main.go before store creation — Phase 3 just needs to add a postgres migration variant - Per-test isolation via NewTestServer is the established pattern — Phase 3 tests can follow the same approach - All acceptance criteria verified: no globals, no SQL in handlers, s.store.X() pattern throughout, main.go wiring complete --- *Phase: 02-backend-refactor* *Completed: 2026-03-23*