Files
DiunDashboard/.planning/codebase/TESTING.md
Jean-Luc Makiola 96c4012e2f chore: add GSD codebase map with 7 analysis documents
Parallel analysis of tech stack, architecture, structure,
conventions, testing patterns, integrations, and concerns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:13:23 +01:00

9.9 KiB

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:

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<Function>_<Scenario> (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:

package diunwebhook_test

import (
    diun "awesomeProject/pkg/diunwebhook"
)

Test Initialization: TestMain resets the database to an in-memory SQLite instance before all tests:

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:

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:

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:
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:

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:

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):

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:

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:

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:

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:

diun.SetWebhookSecret("my-secret")
defer diun.ResetWebhookSecret()
// ... test with/without Authorization header

Testing analysis: 2026-03-23