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>
102 lines
2.4 KiB
Go
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
|
|
}
|