# Testing Patterns **Analysis Date:** 2026-03-23 ## Test Framework **Runner:** - Go standard `testing` package - No third-party test frameworks (no testify, gomega, etc.) - Config: none beyond standard Go tooling **Assertion Style:** - Manual assertions using `t.Errorf` and `t.Fatalf` (no assertion library) - `t.Fatalf` for fatal precondition failures that should stop the test - `t.Errorf` for non-fatal check failures **Run Commands:** ```bash go test -v -coverprofile=coverage.out -coverpkg=./... ./... # All tests with coverage go test -v -run TestWebhookHandler ./pkg/diunwebhook/ # Single test go tool cover -func=coverage.out # View coverage by function go tool cover -html=coverage.out # View coverage in browser ``` ## Test File Organization **Location:** - Co-located with source code in `pkg/diunwebhook/` **Files:** - `pkg/diunwebhook/diunwebhook_test.go` - All tests (external test package `package diunwebhook_test`) - `pkg/diunwebhook/export_test.go` - Test-only exports (internal package `package diunwebhook`) **Naming:** - Test functions: `Test_` (e.g., `TestWebhookHandler_BadRequest`, `TestDismissHandler_NotFound`) - Helper functions: lowercase descriptive names (e.g., `postTag`, `postTagAndGetID`) **Structure:** ``` pkg/diunwebhook/ ├── diunwebhook.go # All production code ├── diunwebhook_test.go # All tests (external package) └── export_test.go # Test-only exports ``` ## Test Structure **External Test Package:** Tests use `package diunwebhook_test` (not `package diunwebhook`), which forces testing through the public API only. The production package is imported with an alias: ```go package diunwebhook_test import ( diun "awesomeProject/pkg/diunwebhook" ) ``` **Test Initialization:** `TestMain` resets the database to an in-memory SQLite instance before all tests: ```go func TestMain(m *testing.M) { diun.UpdatesReset() os.Exit(m.Run()) } ``` **Individual Test Pattern:** Each test resets state at the start, then performs arrange-act-assert: ```go func TestDismissHandler_Success(t *testing.T) { diun.UpdatesReset() // Reset DB err := diun.UpdateEvent(diun.DiunEvent{Image: "nginx:latest"}) // Arrange if err != nil { return } req := httptest.NewRequest(http.MethodPatch, "/api/updates/nginx:latest", nil) // Act rec := httptest.NewRecorder() diun.DismissHandler(rec, req) if rec.Code != http.StatusNoContent { // Assert t.Errorf("expected 204, got %d", rec.Code) } m := diun.GetUpdatesMap() if !m["nginx:latest"].Acknowledged { t.Errorf("expected entry to be acknowledged") } } ``` **Helper Functions:** Test helpers use `t.Helper()` for proper error line reporting: ```go func postTag(t *testing.T, name string) (int, int) { t.Helper() body, _ := json.Marshal(map[string]string{"name": name}) req := httptest.NewRequest(http.MethodPost, "/api/tags", bytes.NewReader(body)) rec := httptest.NewRecorder() diun.TagsHandler(rec, req) return rec.Code, rec.Body.Len() } ``` ## Mocking **Framework:** No mocking framework used **Patterns:** - In-memory SQLite database via `InitDB(":memory:")` replaces the real database - `httptest.NewRequest` and `httptest.NewRecorder` for HTTP handler testing - `httptest.NewServer` for integration-level tests - Custom `failWriter` struct to simulate broken `http.ResponseWriter`: ```go type failWriter struct{ http.ResponseWriter } func (f failWriter) Header() http.Header { return http.Header{} } func (f failWriter) Write([]byte) (int, error) { return 0, errors.New("forced error") } func (f failWriter) WriteHeader(_ int) {} ``` **What to Mock:** - Database: use in-memory SQLite (`:memory:`) - HTTP layer: use `httptest` package - ResponseWriter errors: use custom struct implementing `http.ResponseWriter` **What NOT to Mock:** - Handler logic (test through the HTTP interface) - JSON encoding/decoding (test with real payloads) ## Fixtures and Factories **Test Data:** Events are constructed inline with struct literals: ```go event := diun.DiunEvent{ DiunVersion: "1.0", Hostname: "host", Status: "new", Provider: "docker", Image: "nginx:latest", HubLink: "https://hub.docker.com/nginx", MimeType: "application/json", Digest: "sha256:abc", Created: time.Now(), Platform: "linux/amd64", } ``` Minimal events are also used when only the image field matters: ```go event := diun.DiunEvent{Image: "nginx:latest"} ``` **Location:** - No separate fixtures directory; all test data is inline in `pkg/diunwebhook/diunwebhook_test.go` ## Test-Only Exports **File:** `pkg/diunwebhook/export_test.go` These functions are only accessible to test packages (files ending in `_test.go`): ```go func GetUpdatesMap() map[string]UpdateEntry // Convenience wrapper around GetUpdates() func UpdatesReset() // Re-initializes DB with in-memory SQLite func ResetTags() // Clears tag_assignments and tags tables func ResetWebhookSecret() // Sets webhookSecret to "" ``` ## Coverage **Requirements:** CI warns (does not fail) when coverage drops below 80% **CI Coverage Check:** ```bash go test -v -coverprofile=coverage.out -coverpkg=./... ./... go tool cover -func=coverage.out | tee coverage.txt cov=$(go tool cover -func=coverage.out | grep total: | awk '{print substr($3, 1, length($3)-1)}') cov=${cov%.*} if [ "$cov" -lt 80 ]; then echo "::warning::Test coverage is below 80% ($cov%)" fi ``` **View Coverage:** ```bash go test -coverprofile=coverage.out -coverpkg=./... ./... go tool cover -func=coverage.out # Text summary go tool cover -html=coverage.out # Browser view ``` ## CI Pipeline **Platform:** Gitea Actions (Forgejo-compatible) **CI Workflow:** `.gitea/workflows/ci.yml` - Triggers: push to `develop`, PRs targeting `develop` - Container: custom Docker image with Go and Node.js - Steps: 1. `gofmt -l .` - Formatting check (fails build if unformatted) 2. `go vet ./...` - Static analysis 3. `go test -v -coverprofile=coverage.out -coverpkg=./... ./...` - Tests with coverage 4. Coverage threshold check (80%, warning only) 5. `go build ./...` - Build verification **Release Workflow:** `.gitea/workflows/release.yml` - Triggers: manual dispatch with version bump type (patch/minor/major) - Runs the same build-test job, then creates a Docker image and Gitea release **Missing from CI:** - No frontend build or type-check step - No frontend test step (no frontend tests exist) - No linting beyond `gofmt` and `go vet` ## Test Types **Unit Tests:** - Handler tests using `httptest.NewRequest` / `httptest.NewRecorder` - Direct function tests: `TestUpdateEventAndGetUpdates` - All tests in `pkg/diunwebhook/diunwebhook_test.go` **Concurrency Tests:** - `TestConcurrentUpdateEvent` - 100 concurrent goroutines writing to the database via `sync.WaitGroup` **Integration Tests:** - `TestMainHandlerIntegration` - Full HTTP server via `httptest.NewServer`, tests webhook POST followed by updates GET **Error Path Tests:** - `TestWebhookHandler_BadRequest` - invalid JSON body - `TestWebhookHandler_EmptyImage` - missing required field - `TestWebhookHandler_MethodNotAllowed` - wrong HTTP methods - `TestWebhookHandler_Unauthorized` / `TestWebhookHandler_WrongToken` - auth failures - `TestDismissHandler_NotFound` - dismiss nonexistent entry - `TestDismissHandler_EmptyImage` - empty path parameter - `TestUpdatesHandler_EncodeError` - broken ResponseWriter - `TestCreateTagHandler_DuplicateName` - UNIQUE constraint - `TestCreateTagHandler_EmptyName` - validation **Behavioral Tests:** - `TestDismissHandler_ReappearsAfterNewWebhook` - acknowledged state resets on new webhook - `TestDeleteTagHandler_CascadesAssignment` - tag deletion cascades to assignments - `TestTagAssignmentHandler_Reassign` - reassigning image to different tag - `TestDismissHandler_SlashInImageName` - image names with slashes in URL path **E2E Tests:** - Not implemented - No frontend tests of any kind (no test runner configured, no test files) ## Test Coverage Gaps **Frontend (no tests at all):** - `frontend/src/App.tsx` - main application component - `frontend/src/hooks/useUpdates.ts` - polling, acknowledge, tag assignment logic - `frontend/src/hooks/useTags.ts` - tag CRUD logic - `frontend/src/components/ServiceCard.tsx` - image name parsing, registry detection - `frontend/src/lib/time.ts` - time formatting utilities - `frontend/src/lib/serviceIcons.ts` - icon lookup logic - Priority: Medium (pure utility functions like `getShortName`, `getRegistry`, `timeAgo` would benefit from unit tests) **Backend gaps:** - `cmd/diunwebhook/main.go` - server startup, graceful shutdown, env var reading (not tested) - `TagsHandler` and `TagByIDHandler` method-not-allowed paths for unsupported HTTP methods - `TagAssignmentHandler` bad request paths (missing image, invalid tag_id) - Priority: Low (main.go is thin; handler edge cases are minor) ## Common Patterns **HTTP Handler Testing:** ```go func TestSomeHandler(t *testing.T) { diun.UpdatesReset() // arrange: create test data body, _ := json.Marshal(payload) req := httptest.NewRequest(http.MethodPost, "/path", bytes.NewReader(body)) rec := httptest.NewRecorder() // act diun.SomeHandler(rec, req) // assert status code if rec.Code != http.StatusOK { t.Errorf("expected 200, got %d", rec.Code) } // assert response body var got SomeType json.NewDecoder(rec.Body).Decode(&got) } ``` **State Reset Pattern:** Every test calls `diun.UpdatesReset()` at the start, which re-initializes the in-memory SQLite database. This ensures test isolation without needing parallel-safe fixtures. **Auth Testing Pattern:** ```go diun.SetWebhookSecret("my-secret") defer diun.ResetWebhookSecret() // ... test with/without Authorization header ``` --- *Testing analysis: 2026-03-23*