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:
Run cache tests specifically:
Run with verbose output:
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:
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:
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: , , ,