Skip to content

Testing


Test Organization

The test suite is organized by architectural layer, with each package containing tests for its respective functionality:

asset-db/
├── db_test.go                          # Main package initialization tests
├── cache/
│   ├── cache_test.go                   # Cache infrastructure tests
│   ├── entity_test.go                  # Cache entity operation tests
│   ├── edge_test.go                    # Cache edge operation tests
│   └── tag_test.go                     # Cache tag operation tests
└── repository/
    └── sqlrepo/
        ├── entity_test.go              # SQL entity integration tests
        ├── edge_test.go                # SQL edge integration tests
        └── tag_test.go                 # SQL tag integration tests

Build Tags

The repository uses Go build tags to separate unit tests from integration tests:

Build Tag Purpose Files
None (default) Unit tests that use in-memory databases cache/*_test.go, db_test.go
integration Integration tests requiring real database instances repository/sqlrepo/*_test.go

Sources: ,


Unit Tests

Cache Layer Tests

The cache layer tests validate caching behavior without requiring external database infrastructure. These tests use in-memory SQLite databases for both the cache and persistent storage layers.

Test Repository Creation

flowchart TD
    Test["Test Function"]
    Helper["createTestRepositories()"]
    MemDB["SQLiteMemory Cache<br/>assetdb.New()"]
    FileDB["SQLite DB<br/>assetdb.New()"]
    TempDir["Temporary Directory"]
    Cache["cache.New(cache, db, freq)"]

    Test -->|"Calls"| Helper
    Helper -->|"Creates"| MemDB
    Helper -->|"Creates"| TempDir
    Helper -->|"Creates in"| FileDB
    Helper -->|"Returns both repos"| Test
    Test -->|"Wraps with"| Cache

    style Helper fill:#f9f9f9
    style Cache fill:#fff4e1

Diagram: Cache test infrastructure showing dual repository creation pattern

The createTestRepositories function creates two isolated repositories for testing:

  • Cache Repository: In-memory SQLite (SQLiteMemory) for fast cache operations
  • Persistent Repository: File-based SQLite in a temporary directory
  • Temporary Directory: Automatically cleaned up after tests

Sources:

Cache Test Coverage

Test Function Purpose Key Validations
TestCacheImplementsRepository Interface compliance Verifies Cache implements repository.Repository
TestStartTime Cache initialization timing Validates start time is set correctly on creation
TestGetDBType Database type retrieval Confirms correct database type is returned
TestCreateEntity Entity creation with caching Cache tag creation, eventual consistency with DB
TestCreateAsset Asset creation convenience method Similar to CreateEntity but with simpler API
TestFindEntityById Entity lookup by ID Cache hit behavior
TestFindEntityByContent Content-based entity search Temporal filtering, cache vs. DB queries
TestFindEntitiesByType Type-based entity search Tag-based cache invalidation
TestDeleteEntity Entity deletion Deletion from both cache and DB

Sources: ,

Cache Tag System

The cache uses a tag-based invalidation system to track when data was last synchronized between the cache and persistent database:

graph LR
    subgraph "Cache Tags"
        CCE["cache_create_entity"]
        CCA["cache_create_asset"]
        CFEBT["cache_find_entities_by_type"]
        CIE["cache_incoming_edges"]
        COE["cache_outgoing_edges"]
    end

    subgraph "Operations"
        CE["CreateEntity()"]
        CA["CreateAsset()"]
        FEBT["FindEntitiesByType()"]
        IE["IncomingEdges()"]
        OE["OutgoingEdges()"]
    end

    CE -->|"Creates"| CCE
    CA -->|"Creates"| CCA
    FEBT -->|"Uses"| CFEBT
    IE -->|"Uses"| CIE
    OE -->|"Uses"| COE

    style CCE fill:#f9f9f9
    style CCA fill:#f9f9f9
    style CFEBT fill:#f9f9f9
    style CIE fill:#f9f9f9
    style COE fill:#f9f9f9

Diagram: Cache tag system mapping operations to tag names

Tags store timestamps as SimpleProperty values, enabling temporal queries and cache freshness checks.

Sources: , ,

Main Package Tests

The main package contains basic tests for the initialization function:

// Test from db_test.go
func TestNew(t *testing.T) {
    if _, err := New(sqlrepo.SQLiteMemory, ""); err != nil {
        t.Errorf("Failed to create a new SQLite in-memory repository: %v", err)
    }
}

This validates that the assetdb.New factory function correctly creates an in-memory SQLite repository.

Sources:


Integration Tests

Integration tests validate repository implementations against real database instances. These tests are marked with the //go:build integration build tag and require running database servers.

Test Setup Infrastructure

TestMain and Database Lifecycle

The SQL repository uses a TestMain function to coordinate test execution across multiple database backends:

sequenceDiagram
    participant TM as TestMain
    participant PG as PostgreSQL Setup
    participant SQ as SQLite Setup
    participant Tests as m.Run()
    participant TD as Teardown

    TM->>PG: setupPostgres(dsn)
    PG->>PG: Apply migrations
    PG-->>TM: *gorm.DB

    TM->>Tests: Run all tests against PostgreSQL
    Tests-->>TM: Exit code

    TM->>TD: teardownPostgres(dsn)
    TD->>TD: Rollback migrations

    TM->>SQ: setupSqlite(dsn)
    SQ->>SQ: Apply migrations
    SQ-->>TM: *gorm.DB

    TM->>Tests: Run all tests against SQLite
    Tests-->>TM: Exit code

    TM->>TD: teardownSqlite(dsn)
    TD->>TD: Delete database file

    TM->>TM: os.Exit(code)

Diagram: Integration test lifecycle showing sequential execution across database backends

Sources:

Database-Specific Setup Functions

Function Database Operations
setupPostgres(dsn) PostgreSQL Opens connection, applies migrations using pgmigrations.Migrations()
teardownPostgres(dsn) PostgreSQL Opens connection, rolls back all migrations
setupSqlite(dsn) SQLite Opens connection, applies migrations using sqlitemigrations.Migrations()
teardownSqlite(dsn) SQLite Deletes database file

Sources:

Environment Variables

Integration tests use environment variables for database configuration:

Variable Default Purpose
POSTGRES_USER postgres PostgreSQL username
POSTGRES_PASSWORD postgres PostgreSQL password
POSTGRES_DB postgres PostgreSQL database name
SQLITE3_DB test.db SQLite database file path

Sources:

SQL Repository Test Coverage

Entity Operations

The TestRepository function provides comprehensive coverage of entity and edge operations through multiple test cases:

graph TB
    subgraph "Test Cases"
        TC1["FQDN ↔ FQDN<br/>dns_record"]
        TC2["AutonomousSystem ↔ AutnumRecord<br/>registration"]
        TC3["Netblock ↔ IPAddress<br/>contains"]
        TC4["FQDN ↔ IPAddress<br/>dns_record"]
        TC5["AutonomousSystem ↔ Netblock<br/>announces"]
    end

    subgraph "Operations Tested"
        Create["CreateAsset()"]
        FindID["FindEntityById()"]
        FindContent["FindEntitiesByContent()"]
        FindType["FindEntitiesByType()"]
        CreateEdge["CreateEdge()"]
        Incoming["IncomingEdges()"]
        Outgoing["OutgoingEdges()"]
        Delete["DeleteEdge()<br/>DeleteEntity()"]
    end

    TC1 --> Create
    TC2 --> Create
    TC3 --> Create
    TC4 --> Create
    TC5 --> Create

    Create --> FindID
    FindID --> FindContent
    FindContent --> FindType
    FindType --> CreateEdge
    CreateEdge --> Incoming
    Incoming --> Outgoing
    Outgoing --> Delete

    style TC1 fill:#f9f9f9
    style TC2 fill:#f9f9f9
    style TC3 fill:#f9f9f9
    style TC4 fill:#f9f9f9
    style TC5 fill:#f9f9f9

Diagram: Test case structure showing operations validated per asset type combination

Each test case validates: 1. Entity creation and ID assignment 2. Entity retrieval by ID 3. Entity search by content 4. Entity search by type 5. Edge creation between entities 6. Incoming edge queries 7. Outgoing edge queries 8. Edge and entity deletion

Sources:

LastSeen Update Behavior

The TestLastSeenUpdates function validates that duplicate entity creation updates the LastSeen timestamp while preserving the original CreatedAt timestamp:

sequenceDiagram
    participant Test
    participant Repo as sqlRepository
    participant DB as Database

    Test->>Repo: CreateAsset(IPAddress)
    Repo->>DB: INSERT or UPDATE
    DB-->>Repo: Entity with ID, CreatedAt, LastSeen
    Repo-->>Test: a1

    Note over Test: Sleep 1000ms

    Test->>Repo: CreateAsset(same IPAddress)
    Repo->>DB: INSERT or UPDATE
    DB-->>Repo: Same ID, same CreatedAt, new LastSeen
    Repo-->>Test: a2

    Test->>Test: Assert a1.ID == a2.ID
    Test->>Test: Assert a1.CreatedAt == a2.CreatedAt
    Test->>Test: Assert a2.LastSeen > a1.LastSeen

Diagram: LastSeen update validation sequence

Sources:

Edge Operations

The TestUnfilteredRelations function validates edge creation, querying, and duplicate handling:

graph TB
    Source["Source: FQDN<br/>owasp.com"]
    Dest1["Dest1: FQDN<br/>www.example.owasp.org"]
    Dest2["Dest2: IPAddress<br/>192.168.1.100"]

    Source -->|"dns_record (CNAME)"| Dest1
    Source -->|"dns_record (A)"| Dest2

    subgraph "Validated Operations"
        Out["OutgoingEdges()<br/>with and without filters"]
        In["IncomingEdges()<br/>with and without filters"]
        Dup["Duplicate edge creation<br/>updates LastSeen"]
    end

    Source -.->|"Tests"| Out
    Dest1 -.->|"Tests"| In
    Dest2 -.->|"Tests"| In
    Source -.->|"Tests"| Dup

Diagram: Edge operation test structure showing multi-edge scenario

Sources:

Tag Operations

Tag tests validate the lifecycle of both entity and edge tags:

Test Operations Validated
TestEntityTag CreateEntityProperty(), FindEntityTagById(), GetEntityTags(), DeleteEntityTag()
TestEdgeTag CreateEdgeProperty(), FindEdgeTagById(), GetEdgeTags(), DeleteEdgeTag()

Both tests verify: - Tag creation with correct property name/value - Timestamp initialization (CreatedAt, LastSeen) - Duplicate property handling (updates LastSeen) - Property value updates (creates new tag with new CreatedAt) - Tag retrieval by ID and by entity/edge - Tag deletion

Sources:


Running Tests

Unit Tests (Default)

Run all unit tests including cache tests:

go test ./...

Run cache tests specifically:

go test ./cache/...

Run with verbose output:

go test -v ./cache/...

Sources: ,

Integration Tests

Integration tests require running database instances.

Prerequisites

PostgreSQL:

docker run -d \
  -p 5432:5432 \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_PASSWORD=postgres \
  -e POSTGRES_DB=postgres \
  postgres:latest

SQLite: No external setup required (file-based)

Running Integration Tests

Execute integration tests with the build tag:

go test -tags=integration ./repository/sqlrepo/...

With custom environment variables:

POSTGRES_USER=testuser \
POSTGRES_PASSWORD=testpass \
POSTGRES_DB=testdb \
SQLITE3_DB=/tmp/test.db \
go test -tags=integration ./repository/sqlrepo/...

Verbose output:

go test -tags=integration -v ./repository/sqlrepo/...

Sources: ,


Test Helpers and Utilities

Cache Test Helpers

createTestRepositories Function

flowchart LR
    Func["createTestRepositories()"]

    subgraph "Creates"
        Dir["Temporary Directory<br/>fmt.Sprintf('test-%d', rand.Intn(100))"]
        Cache["In-Memory Cache<br/>assetdb.New(SQLiteMemory, '')"]
        DB["File-Based DB<br/>assetdb.New(SQLite, dir/assetdb.sqlite)"]
    end

    subgraph "Returns"
        R1["cache: repository.Repository"]
        R2["db: repository.Repository"]
        R3["dir: string"]
        R4["err: error"]
    end

    Func --> Dir
    Func --> Cache
    Func --> DB

    Dir --> R3
    Cache --> R1
    DB --> R2

    style Func fill:#f9f9f9

Diagram: createTestRepositories helper function structure

This helper provides: - Isolated test environments via temporary directories - In-memory cache for fast operations - File-based persistent storage for validation - Automatic cleanup via defer in test functions

Sources:

SQL Repository Test Helpers

testSetup Structure

The testSetup struct encapsulates database-specific setup and teardown logic:

type testSetup struct {
    name     string                           // Database type name
    dsn      string                           // Data source name
    setup    func(string) (*gorm.DB, error)  // Setup function
    teardown func(string)                     // Teardown function
}

This abstraction allows TestMain to iterate over multiple database backends with consistent setup/teardown procedures.

Sources:

Migration Helpers

Both setup functions use embedded migration sources:

PostgreSQL:

migrationsSource := migrate.EmbedFileSystemMigrationSource{
    FileSystem: pgmigrations.Migrations(),
    Root:       "/",
}
_, err = migrate.Exec(sqlDb, "postgres", migrationsSource, migrate.Up)

SQLite:

migrationsSource := migrate.EmbedFileSystemMigrationSource{
    FileSystem: sqlitemigrations.Migrations(),
    Root:       "/",
}
_, err = migrate.Exec(sqlDb, "sqlite3", migrationsSource, migrate.Up)

Sources: ,


Test Assertions and Validation Patterns

Testing Framework

All tests use the testify/assert package for assertions:

import "github.com/stretchr/testify/assert"

// Common patterns
assert.NoError(t, err)                    // Verify no error occurred
assert.Error(t, err)                      // Verify error occurred
assert.Equal(t, expected, actual)         // Verify equality
assert.NotEqual(t, notExpected, actual)   // Verify inequality

Sources: ,

Temporal Validation Pattern

Tests frequently validate timestamp behavior:

before := time.Now().Add(-2 * time.Second)
after := time.Now().Add(2 * time.Second)

// Create entity
entity, err := c.CreateEntity(...)

// Validate timestamp within window
if entity.CreatedAt.Before(before) || entity.CreatedAt.After(after) {
    t.Errorf("timestamp outside expected window")
}

This pattern accounts for database timestamp precision and test execution timing.

Sources: ,

Deep Equality for Assets

Asset comparison uses reflect.DeepEqual to validate OAM asset reconstruction:

if !reflect.DeepEqual(entity1.Asset, entity2.Asset) {
    t.Errorf("DeepEqual failed for the assets in the two entities")
}

This ensures proper serialization/deserialization of complex asset types through the database layer.

Sources: ,


Test Coverage Summary

graph TB
    subgraph "Unit Tests (No Build Tag)"
        UT1["Main Package<br/>assetdb.New()"]
        UT2["Cache Layer<br/>All operations"]
    end

    subgraph "Integration Tests (Build Tag: integration)"
        IT1["SQL Repository<br/>PostgreSQL + SQLite"]
        IT2["Entity Operations<br/>CRUD + Queries"]
        IT3["Edge Operations<br/>Relations + Traversal"]
        IT4["Tag Operations<br/>Properties + Metadata"]
    end

    subgraph "Database Backends Tested"
        DB1["SQLite In-Memory"]
        DB2["SQLite File-Based"]
        DB3["PostgreSQL"]
    end

    UT1 --> DB1
    UT2 --> DB1
    UT2 --> DB2

    IT1 --> DB2
    IT1 --> DB3
    IT2 --> IT1
    IT3 --> IT1
    IT4 --> IT1

    style UT1 fill:#f9f9f9
    style UT2 fill:#f9f9f9
    style IT1 fill:#e8e8e8
    style IT2 fill:#e8e8e8
    style IT3 fill:#e8e8e8
    style IT4 fill:#e8e8e8

Diagram: Complete test coverage across layers and database backends

Layer Test Files Databases Coverage
Main Package db_test.go SQLite (memory) Initialization
Cache Layer cache/*_test.go SQLite (memory, file) All operations, tag system, dual-repository pattern
SQL Repository repository/sqlrepo/*_test.go PostgreSQL, SQLite Entities, edges, tags, migrations

Sources: , , ,