rdev/internal/testutil/testutil.go
jordan 72d16929ca feat: Implement hexagonal architecture with services, webhooks, queue, and telemetry
Major refactoring to hexagonal (ports & adapters) architecture:

- Add service layer (apikey_service, project_service) for business logic
- Add webhook system with dispatcher and delivery tracking
- Add command queue with priority-based processing
- Add rate limiting with sliding window algorithm
- Add audit logging for command execution
- Add OpenTelemetry integration (traces, metrics, spans)
- Add circuit breaker for fault tolerance
- Add cached repository wrapper for performance
- Add comprehensive validation package
- Add Kubernetes client integration for pod management
- Add database migrations (allowed_ips, audit_log, rate_limiting, queue, webhooks)
- Add network policy and PodDisruptionBudget for k8s
- Remove legacy executor and projects/registry packages
- Untrack secrets.yaml (now managed via envault)
- Add coverage.out to .gitignore
- Add e2e test infrastructure with docker-compose
- Add comprehensive documentation (API, architecture, operations, plans)
- Add golangci-lint config and pre-commit hook

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:57:46 -07:00

102 lines
2.4 KiB
Go

// Package testutil provides testing utilities for rdev-api.
package testutil
import (
"context"
"database/sql"
"log/slog"
"os"
"testing"
"time"
"github.com/orchard9/rdev/internal/db"
)
// TestDB returns a database connection for testing.
// Uses TEST_DATABASE_URL or falls back to the standard local dev connection.
// Automatically runs migrations to ensure schema is up to date.
func TestDB(t *testing.T) *sql.DB {
t.Helper()
// Use db.New() to get a connection with migrations applied
cfg := db.Config{
Host: "localhost",
Port: 5433,
User: "appuser",
Password: "localdev",
Database: "rdev",
SSLMode: "disable",
}
// Check for override
if dsn := os.Getenv("TEST_DATABASE_URL"); dsn != "" {
// Parse DSN - for simplicity, just use it directly with sql.Open
// This path is for CI/CD environments
rawDB, err := sql.Open("postgres", dsn)
if err != nil {
t.Fatalf("open database: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := rawDB.PingContext(ctx); err != nil {
t.Skipf("database not available: %v", err)
}
t.Cleanup(func() {
_ = rawDB.Close()
})
return rawDB
}
// Use the db package which handles migrations
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
database, err := db.New(cfg, logger)
if err != nil {
// Check if it's a connection error vs migration error
if ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second); true {
defer cancel()
rawDB, openErr := sql.Open("postgres", cfg.DSN())
if openErr == nil {
if pingErr := rawDB.PingContext(ctx); pingErr != nil {
t.Skipf("database not available: %v", pingErr)
}
_ = rawDB.Close()
}
}
t.Fatalf("open database with migrations: %v", err)
}
t.Cleanup(func() {
_ = database.Close()
})
return database.DB
}
// CleanupTestKeys removes all test keys from the database.
func CleanupTestKeys(t *testing.T, db *sql.DB) {
t.Helper()
_, err := db.Exec("DELETE FROM api_keys WHERE name LIKE 'test-%'")
if err != nil {
t.Fatalf("cleanup test keys: %v", err)
}
}
// TimePtr returns a pointer to a time.Time.
func TimePtr(t time.Time) *time.Time {
return &t
}
// MustParseTime parses a time string or panics.
func MustParseTime(layout, value string) time.Time {
t, err := time.Parse(layout, value)
if err != nil {
panic(err)
}
return t
}