Testing

How to run tests, write new tests, and understand the test structure.

Quick Start

# Run all tests
cd backend && go test ./... -count=1

# Run with verbose output
go test ./... -v -count=1

# Run tests for a specific package
go test ./internal/vex/ -v -count=1

# Run a single test by name
go test ./internal/vex/ -run TestNormalizeVulnID -v

# Run with race detection (used in CI)
go test ./... -count=1 -race

# Check coverage
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.out

Test Structure

Tests live next to the code they test, using Go’s _test.go convention:

backend/
├── cmd/
│   └── api-gateway/
│       ├── main.go
│       └── main_test.go            ← auth middleware, input validation
├── internal/
│   ├── clickhouse/
│   │   ├── client.go
│   │   └── client_test.go          ← query method signatures, cluster helpers
│   ├── config/
│   │   ├── config.go
│   │   └── config_test.go          ← Load(), S3 buckets, auth, ignore prefix, shared settings
│   ├── cyclonedx/
│   │   ├── parser.go
│   │   └── parser_test.go          ← CycloneDX parsing
│   ├── github/
│   │   ├── purl.go
│   │   ├── purl_test.go
│   │   ├── resolver.go
│   │   └── resolver_test.go
│   ├── license/
│   │   ├── checker.go
│   │   └── checker_test.go
│   ├── osv/
│   │   ├── client.go
│   │   └── client_test.go
│   ├── osvutil/
│   │   ├── osvutil.go
│   │   └── osvutil_test.go
│   ├── protobomparser/
│   │   ├── parser.go
│   │   └── parser_test.go          ← protobom backend detection
│   ├── repo/
│   │   ├── scanner.go
│   │   └── scanner_test.go         ← file scanning, ignore prefix, generic JSON
│   ├── s3/
│   │   ├── client.go
│   │   └── client_test.go
│   ├── sbom/
│   │   ├── dispatch.go
│   │   └── dispatch_test.go        ← multi-format detection
│   ├── spdx/
│   │   ├── parser.go
│   │   └── parser_test.go
│   └── vex/
│       ├── parser.go
│       └── parser_test.go
├── pkg/
│   ├── dto/
│   │   └── dto_test.go             ← JSON serialization, fields
│   └── models/
│       └── models_test.go          ← cluster fields, omitempty

Current Test Inventory

Counted on 2026-10-04 with CLICKHOUSE_HOST=localhost go test ./... -count=1 -race -json against make dev-up (top-level tests and t.Run subtests that ran). Without CLICKHOUSE_HOST, the four query smoke tests in internal/clickhouse/queries_integration_test.go are skipped — run them whenever you add or change a query, and add new queries to TestQueriesExecute.

PackageTestsSubtestsWhat’s Covered
cmd/api-gateway7077Auth middleware (Bearer/API-Key/disabled), CORS, input validation, public paths, upload, project routes
cmd/ingestion-watcher1111Bucket-prefix stripping, per-object ownership resolution, explicit-config precedence
cmd/mcp-server238Tool surface, transports, Origin/token guards
cmd/parsing-worker3030License pipeline golden test, docs-sync test (see below), license provenance and unresolved-reason precedence, first-party detection, registry resolvers, ownership propagation to every row type incl. all statements in a VEX document
internal/apiclient114Read-only REST client used by the MCP server
internal/clickhouse3754Query helpers, license source labels, package_license_sources padding, query smoke tests against a real ClickHouse (every read query plans and executes; needs CLICKHOUSE_HOST), migration/schema drift (no DB required): migrations replayed into an in-memory schema, every INSERT INTO column asserted to exist, ownership columns required on all data tables, all ingestion_queue writers share one column list, migration numbers unique + contiguous
internal/config456Defaults, env vars, S3 buckets JSON, shared settings inheritance, auth modes, resolver switches
internal/cyclonedx164CycloneDX parsing
internal/depsdev819deps.dev batch + per-version lookups, raw-license recovery, versionless (@*) resolution, negative reasons
internal/docstore278fs/S3 original-document store, reference-driven reads
internal/github2023PURL → repository mapping (well-known table, gopkg.in rule), Resolve, metadata, cache
internal/ingestpath1419Layout parsing and derivation, _ skip token, precedence
internal/license5136Categorize, policy, exceptions, expressions and modes, deprecated IDs, Normalize, Recover, LicenseRef, provenance vocabulary and cache markers
internal/licensetext115License-text classification
internal/npm525Version manifest parsing, latest fallback, not-published / no-license-upstream
internal/nuget58licenseExpression, licenseUrl mapping, GitHub delegation
internal/osv60QueryBatch, errors, cancellation, cache
internal/osvutil535Severity, CVSS, fixed and affected versions
internal/packagist50p2 minified format, dev branches → latest stable, OR-joined license arrays, negatives
internal/projectgroup140Parent resolution signals and ambiguity guard
internal/protobomparser40Backend detection, opt-in dispatch
internal/pypi315PEP 503 names, license_expression > classifiers > free text, no latest fallback for missing versions
internal/repo86File scanning, ignore prefix, generic JSON, SHA256
internal/s31020ClassifyKey, ParseURI, bucket config
internal/sbom1412Multi-format detection (SPDX, CycloneDX, in-toto), Yarn PURL repair
internal/sbomname929Document-name fallback
internal/sourcerepo666source_repo extraction
internal/spdx3339Full parse, in-toto, deterministic IDs, package names
internal/tags710Tag normalisation
internal/vex1213Parse, normalizeVulnID, URL patterns
pkg/dto80JSON serialization
pkg/models60Ownership fields, omitempty
Total524592

License resolution: golden and docs-sync tests

Every license heuristic is a documented decision (see License Resolution). Two tests keep code, behaviour and documentation together:

  • TestLicensePipelineGolden (cmd/parsing-worker/golden_test.go) runs testdata/license_golden.spdx.json — one synthetic package per path through the pipeline — through the real parser, resolvePackageLicenses (with offline registry fakes that implement Explain), the real policy and the compliance exclusion, and pins license, source, category and exclusion per package in licenseGolden.
  • TestDocsLicenseResolutionInSync (cmd/parsing-worker/docs_sync_test.go) parses the tables between <!-- name:start --> / <!-- name:end --> markers on the docs page and asserts: the source vocabulary equals the code’s origins (in resolver order), modifiers and license.UnresolvedReasons; each normalization example matches license.Recover and the default policy; the worked examples equal licenseGolden row for row.

A new rule therefore needs a fixture package, a golden row, a docs row and a decision-log entry; any one missing fails go test.

Test Patterns

Table-Driven Tests

func TestCategorize(t *testing.T) {
    tests := []struct {
        input    string
        expected Category
    }{
        {"MIT", CategoryPermissive},
        {"GPL-3.0-only", CategoryCopyleft},
    }
    for _, tt := range tests {
        t.Run(tt.input, func(t *testing.T) {
            got := Categorize(tt.input)
            if got != tt.expected {
                t.Errorf("Categorize(%q) = %q, want %q", tt.input, got, tt.expected)
            }
        })
    }
}

httptest Mock Server

func TestQueryBatch_MockServer(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"results": [{"vulns": [...]}]}`))
    }))
    defer server.Close()
    // Use server.URL as base URL...
}

t.TempDir() for Filesystem Tests

func TestScanner_Scan(t *testing.T) {
    tmpDir := t.TempDir()
    os.WriteFile(filepath.Join(tmpDir, "test.spdx.json"), []byte(`{...}`), 0644)
    scanner := NewScanner(tmpDir)
    files, err := scanner.Scan()
    // Assert...
}

Test Requirements

  • No external dependencies. No running ClickHouse, network, or Docker needed.
  • No test order dependency. Each test is self-contained.
  • Race-safe. All tests must pass with -race flag.
  • Use subtests (t.Run()) for table-driven tests.

CI Integration

Tests run automatically on every push/PR:

- name: Test
  working-directory: backend
  run: go test ./... -count=1 -race

Angular (Frontend) Tests

The Angular frontend uses Vitest (not Karma/Jasmine). Tests live alongside components as *.spec.ts files.

Quick Start

cd ui

# Run all tests
npx ng test

# Run once (no watch)
npx ng test --watch=false

Current Test Inventory (Frontend)

Spec FileTestsWhat’s Covered
app.spec.ts8App creation, navbar, navigation, global loading bar, router navigation counted as loading
api.service.spec.ts18HTTP methods incl. license sources, error handling, pagination params
archived-packages.component.spec.ts10Data loading, grouped display
dashboard.component.spec.ts3Component creation, data loading, Not Approved license segment
fleet-view.component.spec.ts3Cluster → namespace → project tree
license-overview.component.spec.ts6Category cards (placeholders while loading, error state), License Resolution panel: resolved share, unknown reasons with hints, first-party excluded
project-detail.component.spec.ts12Project detail tabs and stats
project-list.component.spec.ts18Project loading, search, tags, grouping
parent-group-list.component.spec.ts6Parent → project grouping
project-group-list.component.spec.ts9Project → version grouping
sbom-detail.component.spec.ts8Tabs, licenses, dependency license-source tooltip
sbom-list.component.spec.ts10SBOM list loading
cve-impact.component.spec.ts2CVE search, project listing
dependency-stats.component.spec.ts2Top dependencies, unique deps counter
global-search-results.component.spec.ts3Global search results
license-violations.component.spec.ts7Violations tab, exceptions tab, not-approved badge
package-search.spec.ts8Search, expandable results, detail navigation
version-skew.spec.ts3Paginated loading, search
vulnerability-list.component.spec.ts5Vuln list loading, scope filter, loading / empty / error states
global-search.component.spec.ts5Navbar search box
license-source.spec.ts5License source labels, tooltips, reason hints
loading.service.spec.ts8Global loading state: show delay, slow hint, concurrent requests; HTTP interceptor incl. error, unsubscribe, SKIP_GLOBAL_LOADING
loading-bar.component.spec.ts3Loading bar visibility, slow-request hint, ARIA attributes
Total14421 spec files

UI end-to-end tests (Playwright)

The unit specs mock HTTP; the e2e suite in ui/e2e/ drives the built UI through nginx against a live stack, so the /api proxy, security headers, ui-config.json, custom-theme.css and the real API contract are all under test. It is the parity gate for any frontend refactor: the suite must stay green and unchanged.

make dev-up && make demo-fleet && make ui-e2e-wait   # seed examples/fleet, wait for the queue to drain
make ui-e2e                                          # or: cd ui && npm run e2e / npm run e2e:ui
E2E_BASE_URL=http://localhost:4200 npm run e2e       # against `make ui-dev` instead of nginx
SpecRoutesWhat’s covered
app-shell.spec.tsallDark mode (OS preference, toggle, localStorage), CSP + security headers, ui-config.json / custom-theme.css, navbar links, dashboard error state when the API is down
dashboard.spec.ts/, **KPI cards, charts, sanitized description, unknown-route redirect
fleet.spec.ts/fleetFleet link only with clusters, cluster → namespace → project drill-down, stats aside
projects.spec.ts/projects, /projects/:nameSearch, empty state, all tabs incl. virtual-scrolled versions/packages, not-found
sboms.spec.ts/sboms, /sboms/:idFlat/grouped toggle (?view=grouped), search, four tabs, original download
vulnerabilities.spec.ts/vulnerabilities, /cve-impactVirtual list window, id filter, scope filter + CSV export, CVE search, grouping, alias deep link
elements-demo.spec.tspackages/elements/demo.htmlThe web-components bundle (npm run build:elements) mounts outside the Angular app and loads data from the API, and <bomhort-page> routes internally (vuln link → CVE impact) and reports the URL to the host; served by Playwright’s webServer on :8099
licenses.spec.ts/licenses, /license-complianceCategory cards, resolution sources, sort, expand; violations filter (?search=), exceptions tab
search.spec.ts/dependencies, /package-search(/:name), /version-skew, /archived-packages, /searchSorting, searching, pagination, navbar dropdown

Every test fails on a browser console error or a failed /api response (fixture expectApiErrors opts a not-found test out). Tests read their expectations from the API, not from hard-coded demo names. The suite runs with one worker: the gateway rate limit (100 req/10 s per IP) trips with parallel workers.

Performance gate: npm run e2e:bundle-size compares the production build with ui/e2e/bundle-size.baseline.json (initial and total JS, +10 % tolerance). Update it deliberately with node e2e/bundle-size.mjs --update when a size change is intended.

CI runs both in the frontend-e2e job (.github/workflows/ci.yml), uploading the Playwright report and compose logs on failure.

Test Patterns (Angular)

  • Model tests — Verify TypeScript interfaces match API shapes (no HTTP mocking needed)
  • Component tests — Use TestBed with provideHttpClientTesting() for HTTP mocking
  • OnPush strategy — Tests call fixture.detectChanges() and verify DOM output