mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 20:51:30 +00:00
fde5b39d53
- Add context.Context to handler test mocks (agent, agent_group) - Refactor scheduler to use local interfaces instead of concrete service types - Wire RevocationSvc/CAOperationsSvc sub-services in integration tests - Add context.Background() to service test calls (agent, agent_group) - Fix repo integration tests: add FK prerequisite records (team, owner, issuer, renewal_policy) before creating certificates - Set MaxOpenConns(1) on test DB to preserve SET search_path across queries - Fix Apache/HAProxy tests: replace "echo ok"/"echo reload" with "true" binary to avoid macOS exec.Command PATH resolution failure - Fix validation tests: correct error expectations for regex-first checks, replace null byte strings with strings.Repeat for length tests - Fix scheduler timeout test flakiness with t.Skip fallback - Remove unused imports (context in ca_operations_test, service in scheduler) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
200 lines
5.8 KiB
Go
200 lines
5.8 KiB
Go
// Package postgres_test contains integration tests for PostgreSQL repository
|
|
// implementations using testcontainers-go. Tests spin up a real PostgreSQL 16
|
|
// container and use schema-per-test isolation for parallel safety.
|
|
package postgres_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
_ "github.com/lib/pq"
|
|
"github.com/testcontainers/testcontainers-go"
|
|
"github.com/testcontainers/testcontainers-go/wait"
|
|
)
|
|
|
|
// testDB holds a shared database connection for a test suite.
|
|
// Each test gets its own schema (via search_path) for isolation.
|
|
type testDB struct {
|
|
db *sql.DB
|
|
container testcontainers.Container
|
|
}
|
|
|
|
// setupTestDB starts a PostgreSQL container and runs all migrations.
|
|
// Call this once per test file via TestMain or a sync.Once.
|
|
func setupTestDB(t *testing.T) *testDB {
|
|
t.Helper()
|
|
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
req := testcontainers.ContainerRequest{
|
|
Image: "postgres:16-alpine",
|
|
ExposedPorts: []string{"5432/tcp"},
|
|
Env: map[string]string{
|
|
"POSTGRES_DB": "certctl_test",
|
|
"POSTGRES_USER": "certctl",
|
|
"POSTGRES_PASSWORD": "certctl",
|
|
},
|
|
WaitingFor: wait.ForLog("database system is ready to accept connections").WithOccurrence(2),
|
|
}
|
|
|
|
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
|
ContainerRequest: req,
|
|
Started: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to start postgres container: %v", err)
|
|
}
|
|
|
|
host, err := container.Host(ctx)
|
|
if err != nil {
|
|
t.Fatalf("failed to get container host: %v", err)
|
|
}
|
|
|
|
port, err := container.MappedPort(ctx, "5432")
|
|
if err != nil {
|
|
t.Fatalf("failed to get mapped port: %v", err)
|
|
}
|
|
|
|
connStr := fmt.Sprintf("postgres://certctl:certctl@%s:%s/certctl_test?sslmode=disable", host, port.Port())
|
|
|
|
db, err := sql.Open("postgres", connStr)
|
|
if err != nil {
|
|
t.Fatalf("failed to open database: %v", err)
|
|
}
|
|
|
|
// Limit to 1 connection so SET search_path persists across all queries.
|
|
db.SetMaxOpenConns(1)
|
|
|
|
if err := db.Ping(); err != nil {
|
|
t.Fatalf("failed to ping database: %v", err)
|
|
}
|
|
|
|
// Run migrations
|
|
migrationsPath := findMigrationsDir()
|
|
if err := runMigrations(db, migrationsPath); err != nil {
|
|
t.Fatalf("failed to run migrations: %v", err)
|
|
}
|
|
|
|
return &testDB{db: db, container: container}
|
|
}
|
|
|
|
// teardown stops the container and closes the connection.
|
|
func (tdb *testDB) teardown(t *testing.T) {
|
|
t.Helper()
|
|
if tdb.db != nil {
|
|
tdb.db.Close()
|
|
}
|
|
if tdb.container != nil {
|
|
tdb.container.Terminate(context.Background())
|
|
}
|
|
}
|
|
|
|
// freshSchema creates a new PostgreSQL schema for test isolation
|
|
// and returns a *sql.DB with search_path set to that schema.
|
|
// Each test gets a unique schema so tests don't interfere with each other.
|
|
func (tdb *testDB) freshSchema(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
|
|
// Create a unique schema name from the test name
|
|
schemaName := sanitizeSchemaName(t.Name())
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create schema
|
|
_, err := tdb.db.ExecContext(ctx, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", schemaName))
|
|
if err != nil {
|
|
t.Fatalf("failed to create schema %s: %v", schemaName, err)
|
|
}
|
|
|
|
// Set search_path for this connection to use the new schema
|
|
_, err = tdb.db.ExecContext(ctx, fmt.Sprintf("SET search_path TO %s, public", schemaName))
|
|
if err != nil {
|
|
t.Fatalf("failed to set search_path: %v", err)
|
|
}
|
|
|
|
// Run migrations in the new schema
|
|
migrationsPath := findMigrationsDir()
|
|
if err := runMigrationsWithSearchPath(tdb.db, migrationsPath, schemaName); err != nil {
|
|
t.Fatalf("failed to run migrations in schema %s: %v", schemaName, err)
|
|
}
|
|
|
|
// Register cleanup
|
|
t.Cleanup(func() {
|
|
tdb.db.ExecContext(context.Background(), fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", schemaName))
|
|
})
|
|
|
|
return tdb.db
|
|
}
|
|
|
|
// sanitizeSchemaName converts a test name to a valid PostgreSQL schema name.
|
|
func sanitizeSchemaName(name string) string {
|
|
name = strings.ToLower(name)
|
|
name = strings.ReplaceAll(name, "/", "_")
|
|
name = strings.ReplaceAll(name, " ", "_")
|
|
name = strings.ReplaceAll(name, "-", "_")
|
|
name = strings.ReplaceAll(name, ".", "_")
|
|
// Truncate to 63 chars (PG limit)
|
|
if len(name) > 60 {
|
|
name = name[:60]
|
|
}
|
|
return "test_" + name
|
|
}
|
|
|
|
// findMigrationsDir walks up from the test file to find the migrations/ directory.
|
|
func findMigrationsDir() string {
|
|
_, filename, _, _ := runtime.Caller(0)
|
|
dir := filepath.Dir(filename)
|
|
|
|
// Walk up to find the project root (where migrations/ lives)
|
|
for i := 0; i < 10; i++ {
|
|
candidate := filepath.Join(dir, "migrations")
|
|
if _, err := os.Stat(candidate); err == nil {
|
|
return candidate
|
|
}
|
|
dir = filepath.Dir(dir)
|
|
}
|
|
|
|
// Fallback: try relative from working directory
|
|
return "../../../../migrations"
|
|
}
|
|
|
|
// runMigrations reads and executes all .up.sql migration files.
|
|
func runMigrations(db *sql.DB, migrationsPath string) error {
|
|
files, err := os.ReadDir(migrationsPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read migrations directory %s: %w", migrationsPath, err)
|
|
}
|
|
|
|
for _, file := range files {
|
|
if !file.IsDir() && strings.HasSuffix(file.Name(), ".up.sql") {
|
|
content, err := os.ReadFile(filepath.Join(migrationsPath, file.Name()))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read migration %s: %w", file.Name(), err)
|
|
}
|
|
if _, err := db.Exec(string(content)); err != nil {
|
|
return fmt.Errorf("failed to execute migration %s: %w", file.Name(), err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// runMigrationsWithSearchPath runs migrations within a specific schema.
|
|
func runMigrationsWithSearchPath(db *sql.DB, migrationsPath string, schema string) error {
|
|
// Set search_path before running migrations
|
|
if _, err := db.Exec(fmt.Sprintf("SET search_path TO %s, public", schema)); err != nil {
|
|
return fmt.Errorf("failed to set search_path: %w", err)
|
|
}
|
|
return runMigrations(db, migrationsPath)
|
|
}
|