package ejbca_test import ( "context" "crypto/tls" "encoding/base64" "encoding/json" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "github.com/shankar0123/certctl/internal/connector/issuer" "github.com/shankar0123/certctl/internal/connector/issuer/ejbca" ) // Bundle N.A/B-extended: ejbca failure-mode round-out (76.5% → ≥85%). // Targets uncovered branches in IssueCertificate / RevokeCertificate / // GetOrderStatus. func buildEJBCAConnector(t *testing.T, baseURL string) *ejbca.Connector { t.Helper() cfg := &ejbca.Config{ APIUrl: baseURL, AuthMode: "oauth2", Token: "tok", CAName: "TestCA", CertProfile: "TestProfile", EEProfile: "TestEEProfile", } httpClient := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} //nolint:gosec return ejbca.NewWithHTTPClient(cfg, slog.Default(), httpClient) } func TestEJBCA_IssueCertificate_403_ReturnsError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte(`{"error_code":"forbidden"}`)) })) defer srv.Close() c := buildEJBCAConnector(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(), "403") { t.Errorf("expected 403 error, got %v", err) } } func TestEJBCA_IssueCertificate_MalformedJSON(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{not json`)) })) defer srv.Close() c := buildEJBCAConnector(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, got %v", err) } } func TestEJBCA_IssueCertificate_BadCertBase64(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"certificate":"NOT VALID BASE64@@@","certificate_chain":[],"serial_number":"01"}`)) })) defer srv.Close() c := buildEJBCAConnector(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 decode error, got %v", err) } } func TestEJBCA_RevokeCertificate_403_ReturnsError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte(`{}`)) })) defer srv.Close() c := buildEJBCAConnector(t, srv.URL) reason := "keyCompromise" err := c.RevokeCertificate(context.Background(), issuer.RevocationRequest{ Serial: "AB:CD:EF", Reason: &reason, }) if err == nil || !strings.Contains(err.Error(), "403") { t.Errorf("expected 403 error, got %v", err) } } func TestEJBCA_GetOrderStatus_MalformedOrderID(t *testing.T) { c := buildEJBCAConnector(t, "http://example.invalid") st, err := c.GetOrderStatus(context.Background(), "no-double-colons-here") if err != nil { t.Fatalf("GetOrderStatus: %v", err) } if st.Status != "failed" { t.Errorf("expected failed status for malformed order ID, got %q", st.Status) } } func TestEJBCA_GetOrderStatus_404_TreatedAsPending(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer srv.Close() c := buildEJBCAConnector(t, srv.URL) st, err := c.GetOrderStatus(context.Background(), "CN=Issuer::AB:CD") if err != nil { t.Fatalf("GetOrderStatus: %v", err) } if st.Status != "pending" { t.Errorf("expected pending for 404 (cert not yet issued), got %q", st.Status) } } func TestEJBCA_GetOrderStatus_HappyPath(t *testing.T) { // Build a tiny self-signed DER cert for the round-trip derBytes := []byte{ 0x30, 0x82, 0x00, 0x10, // junk DER prefix to pass base64 decode } _ = derBytes // Simpler: just confirm 200 with valid base64 attempts to parse and fails cleanly srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"certificate":"` + base64.StdEncoding.EncodeToString([]byte("fake")) + `","certificate_chain":[],"serial_number":"AB:CD"}`)) })) defer srv.Close() c := buildEJBCAConnector(t, srv.URL) _, err := c.GetOrderStatus(context.Background(), "CN=Issuer::AB:CD") if err == nil || !strings.Contains(err.Error(), "parse certificate") { t.Errorf("expected x509 parse error, got %v", err) } } func TestEJBCA_GetOrderStatus_MalformedJSON(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{not json`)) })) defer srv.Close() c := buildEJBCAConnector(t, srv.URL) _, err := c.GetOrderStatus(context.Background(), "CN=Issuer::AB:CD") if err == nil || !strings.Contains(err.Error(), "parse") { t.Errorf("expected parse error, got %v", err) } } func TestEJBCA_RevokeCertificate_NilReason_Defaults(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"revocation_status":"revoked"}`)) })) defer srv.Close() c := buildEJBCAConnector(t, srv.URL) // Reason=nil exercises the default-reason branch. err := c.RevokeCertificate(context.Background(), issuer.RevocationRequest{ Serial: "AB:CD:EF", }) if err != nil { t.Errorf("expected nil-reason revoke to succeed, got %v", err) } } func TestEJBCA_IssueCertificate_500_PropagatesError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`internal error`)) })) defer srv.Close() c := buildEJBCAConnector(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(), "500") { t.Errorf("expected 500 error, got %v", err) } } func TestEJBCA_GetOrderStatus_BadCertBase64(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"certificate":"NOT VALID BASE64@@@","certificate_chain":[]}`)) })) defer srv.Close() c := buildEJBCAConnector(t, srv.URL) _, err := c.GetOrderStatus(context.Background(), "CN=Issuer::AB:CD") if err == nil { t.Errorf("expected error from bad base64") } // json package's strict typing — this might not even reach base64 decoding // if certificate field has invalid base64. Either way, error is fine. _ = json.Marshal }