mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 18:41:30 +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.
438 lines
11 KiB
Go
438 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestComputeCertificateFingerprint(t *testing.T) {
|
|
// Generate a test certificate for fingerprint validation
|
|
cert, err := generateTestCert()
|
|
if err != nil {
|
|
t.Fatalf("failed to generate test cert: %v", err)
|
|
}
|
|
|
|
certPEM := string(pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: cert.Raw,
|
|
}))
|
|
|
|
fp, err := computeCertificateFingerprint(certPEM)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(fp) != 64 { // SHA256 hex = 64 chars
|
|
t.Errorf("expected 64 char fingerprint, got %d", len(fp))
|
|
}
|
|
}
|
|
|
|
func TestComputeCertificateFingerprint_InvalidPEM(t *testing.T) {
|
|
_, err := computeCertificateFingerprint("not a valid pem")
|
|
if err == nil {
|
|
t.Error("expected error for invalid PEM")
|
|
}
|
|
}
|
|
|
|
func TestComputeCertificateFingerprint_EmptyString(t *testing.T) {
|
|
_, err := computeCertificateFingerprint("")
|
|
if err == nil {
|
|
t.Error("expected error for empty string")
|
|
}
|
|
}
|
|
|
|
func TestExtractTargetHostAndPort_ValidConfig(t *testing.T) {
|
|
config := map[string]interface{}{
|
|
"host": "example.com",
|
|
"port": 443.0,
|
|
}
|
|
configJSON, _ := json.Marshal(config)
|
|
|
|
host, port, err := extractTargetHostAndPort(configJSON)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if host != "example.com" {
|
|
t.Errorf("expected host example.com, got %s", host)
|
|
}
|
|
if port != 443 {
|
|
t.Errorf("expected port 443, got %d", port)
|
|
}
|
|
}
|
|
|
|
func TestExtractTargetHostAndPort_DefaultPort(t *testing.T) {
|
|
config := map[string]interface{}{
|
|
"hostname": "test.local",
|
|
}
|
|
configJSON, _ := json.Marshal(config)
|
|
|
|
host, port, err := extractTargetHostAndPort(configJSON)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if host != "test.local" {
|
|
t.Errorf("expected host test.local, got %s", host)
|
|
}
|
|
if port != 443 {
|
|
t.Errorf("expected default port 443, got %d", port)
|
|
}
|
|
}
|
|
|
|
func TestExtractTargetHostAndPort_MissingHost(t *testing.T) {
|
|
config := map[string]interface{}{
|
|
"port": 443.0,
|
|
}
|
|
configJSON, _ := json.Marshal(config)
|
|
|
|
_, _, err := extractTargetHostAndPort(configJSON)
|
|
if err == nil {
|
|
t.Error("expected error for missing host")
|
|
}
|
|
}
|
|
|
|
func TestExtractTargetHostAndPort_InvalidJSON(t *testing.T) {
|
|
configJSON := []byte("invalid json{")
|
|
|
|
_, _, err := extractTargetHostAndPort(configJSON)
|
|
if err == nil {
|
|
t.Error("expected error for invalid JSON")
|
|
}
|
|
}
|
|
|
|
func TestExtractTargetHostAndPort_AlternativeFieldNames(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config map[string]interface{}
|
|
expected string
|
|
}{
|
|
{"host", map[string]interface{}{"host": "host1.com"}, "host1.com"},
|
|
{"hostname", map[string]interface{}{"hostname": "host2.com"}, "host2.com"},
|
|
{"target", map[string]interface{}{"target": "host3.com"}, "host3.com"},
|
|
{"address", map[string]interface{}{"address": "host4.com"}, "host4.com"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
configJSON, _ := json.Marshal(tt.config)
|
|
host, _, err := extractTargetHostAndPort(configJSON)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if host != tt.expected {
|
|
t.Errorf("expected %s, got %s", tt.expected, host)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerifyDeployment_Timeout(t *testing.T) {
|
|
cert, _ := generateTestCert()
|
|
certPEM := string(pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: cert.Raw,
|
|
}))
|
|
|
|
ctx := context.Background()
|
|
result, err := verifyDeployment(ctx, "192.0.2.1", 443, certPEM, 0, 100*time.Millisecond, nil)
|
|
|
|
// Connection to reserved test IP should timeout or fail
|
|
if err == nil && result == nil {
|
|
t.Error("expected error or result for unreachable host")
|
|
}
|
|
}
|
|
|
|
func TestVerifyDeployment_InvalidCertPEM(t *testing.T) {
|
|
ctx := context.Background()
|
|
result, err := verifyDeployment(ctx, "localhost", 443, "not a cert", 0, 5*time.Second, nil)
|
|
|
|
if err == nil {
|
|
t.Error("expected error for invalid certificate PEM")
|
|
}
|
|
if result != nil {
|
|
t.Error("expected no result on error")
|
|
}
|
|
}
|
|
|
|
// Helper function to generate a test certificate for testing
|
|
func generateTestCert() (*x509.Certificate, error) {
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{
|
|
CommonName: "test.example.com",
|
|
},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(24 * time.Hour),
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
BasicConstraintsValid: true,
|
|
DNSNames: []string{"test.example.com"},
|
|
}
|
|
|
|
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return x509.ParseCertificate(certDER)
|
|
}
|
|
|
|
func TestReportVerificationResult_Success(t *testing.T) {
|
|
// Create mock HTTP server
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/jobs/j-test/verify" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
if r.Method != "POST" {
|
|
t.Errorf("unexpected method: %s", r.Method)
|
|
}
|
|
|
|
// Check auth header
|
|
auth := r.Header.Get("Authorization")
|
|
if auth != "Bearer test-api-key" {
|
|
t.Errorf("unexpected auth header: %s", auth)
|
|
}
|
|
|
|
// Verify request body
|
|
var payload map[string]interface{}
|
|
json.NewDecoder(r.Body).Decode(&payload)
|
|
if payload["verified"] != true {
|
|
t.Error("expected verified to be true")
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"job_id": "j-test",
|
|
"verified": true,
|
|
})
|
|
}))
|
|
defer server.Close()
|
|
|
|
cfg := &AgentConfig{
|
|
ServerURL: server.URL,
|
|
APIKey: "test-api-key",
|
|
}
|
|
agent, _ := NewAgent(cfg, nil)
|
|
|
|
result := &VerificationResult{
|
|
ExpectedFingerprint: "abc123",
|
|
ActualFingerprint: "abc123",
|
|
Verified: true,
|
|
VerifiedAt: time.Now().UTC(),
|
|
}
|
|
|
|
err := agent.reportVerificationResult(context.Background(), "j-test", "t-nginx1", result)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReportVerificationResult_MissingFields(t *testing.T) {
|
|
agent, _ := NewAgent(&AgentConfig{}, nil)
|
|
|
|
result := &VerificationResult{
|
|
Verified: true,
|
|
VerifiedAt: time.Now().UTC(),
|
|
}
|
|
|
|
err := agent.reportVerificationResult(context.Background(), "", "t-nginx1", result)
|
|
if err == nil {
|
|
t.Error("expected error for missing job ID")
|
|
}
|
|
}
|
|
|
|
func TestVerifyDeployment_ContextCancellation(t *testing.T) {
|
|
cert, _ := generateTestCert()
|
|
certPEM := string(pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: cert.Raw,
|
|
}))
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
result, err := verifyDeployment(ctx, "localhost", 443, certPEM, 1*time.Second, 5*time.Second, nil)
|
|
|
|
if err == nil {
|
|
t.Error("expected error for cancelled context")
|
|
}
|
|
if result != nil {
|
|
t.Error("expected no result on context cancellation")
|
|
}
|
|
}
|
|
|
|
// Mock TLS server for verification testing.
|
|
// Reserved for future use when real TLS verification integration tests are added.
|
|
var _ = func(t *testing.T, cert *x509.Certificate) (string, func()) {
|
|
// Create TLS listener with test certificate
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("failed to create listener: %v", err)
|
|
}
|
|
|
|
address := listener.Addr().String()
|
|
|
|
go func() {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
// Simple echo to keep connection alive
|
|
buf := make([]byte, 1024)
|
|
conn.Read(buf) //nolint:errcheck
|
|
}()
|
|
|
|
cleanup := func() {
|
|
listener.Close()
|
|
}
|
|
|
|
return address, cleanup
|
|
}
|
|
|
|
func TestVerificationResult_JSONMarshaling(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
result := &VerificationResult{
|
|
ExpectedFingerprint: "abc123",
|
|
ActualFingerprint: "def456",
|
|
Verified: false,
|
|
VerifiedAt: now,
|
|
Error: "fingerprint mismatch",
|
|
}
|
|
|
|
data, err := json.Marshal(result)
|
|
if err != nil {
|
|
t.Errorf("unexpected error marshaling: %v", err)
|
|
}
|
|
|
|
var unmarshaled VerificationResult
|
|
err = json.Unmarshal(data, &unmarshaled)
|
|
if err != nil {
|
|
t.Errorf("unexpected error unmarshaling: %v", err)
|
|
}
|
|
|
|
if unmarshaled.Error != "fingerprint mismatch" {
|
|
t.Errorf("error mismatch: got %s", unmarshaled.Error)
|
|
}
|
|
}
|
|
|
|
func TestReportVerificationResult_ServerError(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("server error"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
cfg := &AgentConfig{
|
|
ServerURL: server.URL,
|
|
APIKey: "test-api-key",
|
|
}
|
|
agent, _ := NewAgent(cfg, nil)
|
|
|
|
result := &VerificationResult{
|
|
ExpectedFingerprint: "abc123",
|
|
ActualFingerprint: "abc123",
|
|
Verified: true,
|
|
VerifiedAt: time.Now().UTC(),
|
|
}
|
|
|
|
err := agent.reportVerificationResult(context.Background(), "j-test", "t-nginx1", result)
|
|
if err == nil {
|
|
t.Error("expected error for server error response")
|
|
}
|
|
}
|
|
|
|
func TestExtractTargetHostAndPort_InvalidPort(t *testing.T) {
|
|
config := map[string]interface{}{
|
|
"host": "example.com",
|
|
"port": 99999.0,
|
|
}
|
|
configJSON, _ := json.Marshal(config)
|
|
|
|
_, _, err := extractTargetHostAndPort(configJSON)
|
|
if err == nil {
|
|
t.Error("expected error for invalid port")
|
|
}
|
|
}
|
|
|
|
func TestExtractTargetHostAndPort_ZeroPort(t *testing.T) {
|
|
config := map[string]interface{}{
|
|
"host": "example.com",
|
|
"port": 0.0,
|
|
}
|
|
configJSON, _ := json.Marshal(config)
|
|
|
|
_, _, err := extractTargetHostAndPort(configJSON)
|
|
if err == nil {
|
|
t.Error("expected error for zero port")
|
|
}
|
|
}
|
|
|
|
func TestVerifyDeployment_FingerprintComparison(t *testing.T) {
|
|
// Create a simple TLS server for testing
|
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer server.Close()
|
|
|
|
// Q-1 closure (cat-s3-58ce7e9840be): defensive skip — httptest.NewTLSServer
|
|
// always provisions a self-signed certificate at construction time, so this
|
|
// branch is currently unreachable in practice. Kept as a guard against
|
|
// future test-server constructions that swap in a custom *tls.Config with
|
|
// no Certificates slice (the path below dereferences server.TLS.Certificates[0]
|
|
// and would panic). The skip preserves the assertion logic for the normal
|
|
// fixture path; if it ever fires, it's a fixture bug, not a product bug.
|
|
if len(server.TLS.Certificates) == 0 {
|
|
t.Skip("no TLS certificates configured on test server")
|
|
}
|
|
|
|
// Parse the leaf certificate from the DER bytes
|
|
leafDER := server.TLS.Certificates[0].Certificate[0]
|
|
leafCert, err := x509.ParseCertificate(leafDER)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse test server certificate: %v", err)
|
|
}
|
|
|
|
certPEM := string(pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: leafCert.Raw,
|
|
}))
|
|
|
|
// Get host and port from the listener address
|
|
addr := server.Listener.Addr().String()
|
|
host, portStr, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse server address: %v", err)
|
|
}
|
|
port := 0
|
|
fmt.Sscanf(portStr, "%d", &port)
|
|
|
|
// Verify deployment against the live TLS server
|
|
ctx := context.Background()
|
|
result, _ := verifyDeployment(ctx, host, port, certPEM, 0, 5*time.Second, nil)
|
|
|
|
// This test may fail in some environments due to TLS setup complexity
|
|
// The key is testing the fingerprint comparison logic
|
|
if result != nil {
|
|
if result.Verified && result.ExpectedFingerprint != result.ActualFingerprint {
|
|
t.Error("fingerprint mismatch: expected and actual should match if Verified is true")
|
|
}
|
|
}
|
|
}
|