package vault_test import ( "context" "encoding/json" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "github.com/certctl-io/certctl/internal/connector/issuer" "github.com/certctl-io/certctl/internal/connector/issuer/vault" "github.com/certctl-io/certctl/internal/secret" ) // Bundle N.A/B-extended: failure-mode round-out for Vault PKI connector. // Exercises uncovered branches in IssueCertificate (malformed response, // empty cert, structured Vault error format) and GetCACertPEM (non-200, // connection error). Pushes vault 84.1% → ≥85%. func TestVault_IssueCertificate_StructuredVaultError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.HasSuffix(r.URL.Path, "/sys/health"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"initialized":true,"sealed":false,"standby":false}`)) default: w.WriteHeader(http.StatusBadRequest) // Vault's structured error format: {"errors": [...]} _ = json.NewEncoder(w).Encode(map[string]interface{}{ "errors": []string{"role policy missing", "ttl exceeds max"}, }) } })) defer srv.Close() c := buildVaultConnector(t, srv.URL) _, err := c.IssueCertificate(context.Background(), issuer.IssuanceRequest{ CommonName: "x.example.com", CSRPEM: "-----BEGIN CERTIFICATE REQUEST-----\nfake\n-----END CERTIFICATE REQUEST-----", }) if err == nil { t.Fatalf("expected error for 400 with structured Vault errors") } if !strings.Contains(err.Error(), "role policy missing") { t.Errorf("expected error to surface Vault's structured errors, got %v", err) } } func TestVault_IssueCertificate_MalformedResponseJSON(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.HasSuffix(r.URL.Path, "/sys/health"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"initialized":true,"sealed":false,"standby":false}`)) default: w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{not valid json`)) } })) defer srv.Close() c := buildVaultConnector(t, srv.URL) _, err := c.IssueCertificate(context.Background(), issuer.IssuanceRequest{ CommonName: "x.example.com", CSRPEM: "-----BEGIN CERTIFICATE REQUEST-----\nfake\n-----END CERTIFICATE REQUEST-----", }) if err == nil || !strings.Contains(err.Error(), "parse") { t.Errorf("expected parse error for malformed JSON, got %v", err) } } func TestVault_IssueCertificate_EmptyCertificate(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.HasSuffix(r.URL.Path, "/sys/health"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"initialized":true,"sealed":false,"standby":false}`)) default: w.WriteHeader(http.StatusOK) // Vault response shape with empty certificate field _, _ = w.Write([]byte(`{"data":{"certificate":"","serial_number":"01:02:03"}}`)) } })) defer srv.Close() c := buildVaultConnector(t, srv.URL) _, err := c.IssueCertificate(context.Background(), issuer.IssuanceRequest{ CommonName: "x.example.com", CSRPEM: "-----BEGIN CERTIFICATE REQUEST-----\nfake\n-----END CERTIFICATE REQUEST-----", }) if err == nil || !strings.Contains(err.Error(), "no certificate") { t.Errorf("expected 'no certificate' error, got %v", err) } } func TestVault_IssueCertificate_MalformedCertPEM(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.HasSuffix(r.URL.Path, "/sys/health"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"initialized":true,"sealed":false,"standby":false}`)) default: w.WriteHeader(http.StatusOK) // Cert is non-PEM garbage _, _ = w.Write([]byte(`{"data":{"certificate":"not-a-pem-block","serial_number":"01"}}`)) } })) defer srv.Close() c := buildVaultConnector(t, srv.URL) _, err := c.IssueCertificate(context.Background(), issuer.IssuanceRequest{ CommonName: "x.example.com", CSRPEM: "-----BEGIN CERTIFICATE REQUEST-----\nfake\n-----END CERTIFICATE REQUEST-----", }) if err == nil || !strings.Contains(err.Error(), "decode") { t.Errorf("expected PEM-decode error, got %v", err) } } func TestVault_GetCACertPEM_Non200_ReturnsError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.HasSuffix(r.URL.Path, "/sys/health"): w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"initialized":true,"sealed":false,"standby":false}`)) default: // CA cert endpoint returns 403 w.WriteHeader(http.StatusForbidden) } })) defer srv.Close() c := buildVaultConnector(t, srv.URL) _, err := c.GetCACertPEM(context.Background()) if err == nil || !strings.Contains(err.Error(), "403") { t.Errorf("expected 403 error, got %v", err) } } // buildVaultConnector constructs a vault.Connector pointed at the given URL // by going through ValidateConfig (which the existing test pattern uses). func buildVaultConnector(t *testing.T, url string) *vault.Connector { t.Helper() c := vault.New(nil, slog.Default()) cfg := vault.Config{Addr: url, Token: secret.NewRefFromString("tok"), Mount: "pki", Role: "web", TTL: "1h"} raw, _ := json.Marshal(cfg) if err := c.ValidateConfig(context.Background(), raw); err != nil { t.Fatalf("ValidateConfig: %v", err) } return c }