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

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