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

310 lines
9.9 KiB
Markdown

# 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<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:
```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*