mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 22:01:36 +00:00
90bfa5d320
Closes Q-1 (cat-s3-58ce7e9840be) — 37 t.Skip / testing.Short() sites
across 9 test files audited. Per-site verdict matrix:
- cmd/agent/verify_test.go (1 site): defensive guard against unreachable
httptest.NewTLSServer code path. Document-skip with closure comment.
- deploy/test/qa_test.go (11 sites): file already gated by `//go:build qa`
tag. The 11 t.Skip("Requires X — manual test") markers are runtime
second-line guards for operators who run -tags qa against a stack
missing the required external service. File-level header comment
block added explaining the manual-test convention.
- deploy/test/healthcheck_test.go (5 sites): 3 docker-availability +
1 testing.Short + 1 hard-skip for not-yet-wired runtime probe
(image-spec contract above already covers the audit-flagged
regression). All correctly gated; file-level header comment block
added explaining each.
- deploy/test/integration_test.go (5 sites): in-flight-state guards
(poll-with-skip after 90s polling for agent-online, inter-test
Phase04→Phase07 ordering, scheduler-tick race for discovered certs,
inter-test issuer fallthrough, defensive PEM-empty assertion).
Each site now has a closure comment explaining why skip is the
right choice rather than fail (upstream phase already surfaces the
real failure; skipping prevents masking root cause behind cascading
noise).
- internal/repository/postgres/{testutil,seed,repo}_test.go (5 sites):
testing.Short() gates for testcontainers-backed live PostgreSQL
integration tests. All correctly gated; closure comments added
naming the run command.
- internal/connector/notifier/email/email_test.go (2 sites):
anti-fixture assertions (test asserts SMTP dial fails; if a captive
portal black-holes the call to success, skip rather than false-pass).
Closure comments added explaining the fixture assumption.
- internal/connector/target/iis/iis_test.go (2 sites): platform-gated
skip for powershell.exe absence on non-Windows hosts. Mirrors the
production iis_connector.go LookPath guard. Closure comments added.
Total: 17 closure comments anchor the 37 skip sites (some sites share a
single block-level comment). All skips remain in place; the change is
purely documentation. The audit recommendation was "audit each skip and
decide" — for these 37, the decision is uniformly **document-skip**:
the gating is correct, the t.Skip messages name the missing precondition,
and the closure comments now pin the rationale for future readers.
See coverage-gap-audit-2026-04-24-v5/unified-audit.md
cat-s3-58ce7e9840be for closure rationale.
205 lines
6.1 KiB
Go
205 lines
6.1 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()
|
|
|
|
// Q-1 closure (cat-s3-58ce7e9840be): live PostgreSQL needed via
|
|
// testcontainers-go (postgres:16-alpine). Run with:
|
|
// go test -count=1 ./internal/repository/postgres/... (omit -short)
|
|
// The short-mode gate keeps it off the default `go test ./... -short`
|
|
// fast loop where docker-in-docker may not be available.
|
|
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)
|
|
}
|