Merge fix/coverage-N.AB-extended: Bundle N.A/B-extended — 6 connectors lifted; M-001 closed

This commit is contained in:
cowork
2026-04-27 21:35:01 +00:00
6 changed files with 1078 additions and 0 deletions
@@ -0,0 +1,168 @@
package digicert_test
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/shankar0123/certctl/internal/connector/issuer/digicert"
)
// Bundle N.A/B-extended: digicert failure-mode round-out (81.0% → ≥85%).
// Targets GetOrderStatus / downloadCertificate / parsePEMBundle uncovered
// branches.
func buildDigicertConnector(t *testing.T, baseURL string) *digicert.Connector {
t.Helper()
c := digicert.New(nil, slog.Default())
cfg := digicert.Config{APIKey: "k", OrgID: "1", ProductType: "ssl_basic", BaseURL: baseURL}
raw, _ := json.Marshal(cfg)
if err := c.ValidateConfig(context.Background(), raw); err != nil {
t.Fatalf("ValidateConfig: %v", err)
}
return c
}
func TestDigicert_GetOrderStatus_404_ReturnsError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/user/me":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":1}`))
default:
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"errors":[{"code":"order_not_found"}]}`))
}
}))
defer srv.Close()
c := buildDigicertConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "missing-order")
if err == nil || !strings.Contains(err.Error(), "404") {
t.Errorf("expected 404 error, got %v", err)
}
}
func TestDigicert_GetOrderStatus_MalformedJSON_ReturnsError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/user/me":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":1}`))
default:
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{not valid json`))
}
}))
defer srv.Close()
c := buildDigicertConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "bad-order")
if err == nil || !strings.Contains(err.Error(), "parse") {
t.Errorf("expected parse error, got %v", err)
}
}
func TestDigicert_GetOrderStatus_IssuedButCertIDMissing(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/user/me":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":1}`))
default:
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"issued","certificate":{"id":0}}`))
}
}))
defer srv.Close()
c := buildDigicertConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "issued-no-cert-id")
if err == nil || !strings.Contains(err.Error(), "certificate_id is missing") {
t.Errorf("expected 'certificate_id is missing' error, got %v", err)
}
}
func TestDigicert_GetOrderStatus_PendingProcessingDeniedUnknown(t *testing.T) {
cases := []struct {
name string
status string
wantStatus string
}{
{"pending", "pending", "pending"},
{"processing", "processing", "pending"},
{"rejected", "rejected", "failed"},
{"denied", "denied", "failed"},
{"unknown", "frobnicating", "pending"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/user/me":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":1}`))
default:
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"` + tc.status + `"}`))
}
}))
defer srv.Close()
c := buildDigicertConnector(t, srv.URL)
st, err := c.GetOrderStatus(context.Background(), "order-x")
if err != nil {
t.Fatalf("GetOrderStatus: %v", err)
}
if st.Status != tc.wantStatus {
t.Errorf("expected status=%q for input=%q, got %q", tc.wantStatus, tc.status, st.Status)
}
})
}
}
func TestDigicert_DownloadCertificate_Non200_ReturnsError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/user/me":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":1}`))
case strings.Contains(r.URL.Path, "/certificate/"):
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte(`{"errors":[{"code":"forbidden"}]}`))
default:
// /order/certificate/<id> returns issued with cert_id 7
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"issued","certificate":{"id":7}}`))
}
}))
defer srv.Close()
c := buildDigicertConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "order-y")
if err == nil || !strings.Contains(err.Error(), "403") {
t.Errorf("expected 403 download error, got %v", err)
}
}
func TestDigicert_DownloadCertificate_MalformedPEM_ReturnsError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/user/me":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"id":1}`))
case strings.Contains(r.URL.Path, "/certificate/") && strings.Contains(r.URL.Path, "/download/"):
// Returns junk that won't decode as PEM
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("not a pem bundle"))
default:
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"issued","certificate":{"id":42}}`))
}
}))
defer srv.Close()
c := buildDigicertConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "order-z")
if err == nil {
t.Errorf("expected error from malformed PEM bundle, got nil")
}
}
@@ -0,0 +1,205 @@
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
}
@@ -0,0 +1,204 @@
package entrust
import (
"context"
"crypto/tls"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// Bundle N.A/B-extended: entrust failure-mode round-out (70.8% → ≥85%).
// Targets uncovered branches in ValidateConfig / GetOrderStatus /
// loadMTLSConfig / parseCertMetadata / mapRevocationReason.
//
// In-package (white-box) tests so we can exercise unexported helpers
// directly.
func buildEntrustConnector(t *testing.T, baseURL string) *Connector {
t.Helper()
cfg := &Config{
APIUrl: baseURL,
CAId: "test-ca-id",
}
httpClient := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} //nolint:gosec
return NewWithHTTPClient(cfg, slog.Default(), httpClient)
}
// ─────────────────────────────────────────────────────────────────────────────
// mapRevocationReason: every RFC 5280 reason string + nil + default
// ─────────────────────────────────────────────────────────────────────────────
func TestEntrust_MapRevocationReason_AllArms(t *testing.T) {
cases := []struct {
reason *string
expected string
}{
{nil, "Unspecified"},
{strPtr(""), "Unspecified"},
{strPtr("unspecified"), "Unspecified"},
{strPtr("keyCompromise"), "KeyCompromise"},
{strPtr("caCompromise"), "CACompromise"},
{strPtr("affiliationChanged"), "AffiliationChanged"},
{strPtr("superseded"), "Superseded"},
{strPtr("cessationOfOperation"), "CessationOfOperation"},
{strPtr("certificateHold"), "CertificateHold"},
{strPtr("privilegeWithdrawn"), "PrivilegeWithdrawn"},
{strPtr("frobnicated"), "Unspecified"}, // unknown → default
}
for _, tc := range cases {
name := "nil"
if tc.reason != nil {
name = *tc.reason
if name == "" {
name = "empty"
}
}
t.Run(name, func(t *testing.T) {
got := mapRevocationReason(tc.reason)
if got != tc.expected {
t.Errorf("expected %q, got %q", tc.expected, got)
}
})
}
}
func strPtr(s string) *string { return &s }
// ─────────────────────────────────────────────────────────────────────────────
// parseCertMetadata: malformed-PEM + bad-DER branches
// ─────────────────────────────────────────────────────────────────────────────
func TestEntrust_ParseCertMetadata_NotPEM(t *testing.T) {
_, _, _, err := parseCertMetadata("not a pem block")
if err == nil || !strings.Contains(err.Error(), "decode") {
t.Errorf("expected decode error, got %v", err)
}
}
func TestEntrust_ParseCertMetadata_BadDER(t *testing.T) {
pemBlock := "-----BEGIN CERTIFICATE-----\nbm90LWEtZGVy\n-----END CERTIFICATE-----\n"
_, _, _, err := parseCertMetadata(pemBlock)
if err == nil || !strings.Contains(err.Error(), "parse") {
t.Errorf("expected parse error, got %v", err)
}
}
// ─────────────────────────────────────────────────────────────────────────────
// loadMTLSConfig: nonexistent file + nonexistent key
// ─────────────────────────────────────────────────────────────────────────────
func TestEntrust_LoadMTLSConfig_NonexistentFile(t *testing.T) {
_, err := loadMTLSConfig("/nonexistent/cert.pem", "/nonexistent/key.pem")
if err == nil || !strings.Contains(err.Error(), "load client certificate") {
t.Errorf("expected load error, got %v", err)
}
}
// ─────────────────────────────────────────────────────────────────────────────
// ValidateConfig: required-field misses + unreachable URL
// ─────────────────────────────────────────────────────────────────────────────
func TestEntrust_ValidateConfig_MissingFields(t *testing.T) {
cases := []struct {
name string
cfg Config
want string
}{
{"missing api_url", Config{ClientCertPath: "/c", ClientKeyPath: "/k", CAId: "ca"}, "api_url"},
{"missing client_cert_path", Config{APIUrl: "http://x", ClientKeyPath: "/k", CAId: "ca"}, "client_cert_path"},
{"missing client_key_path", Config{APIUrl: "http://x", ClientCertPath: "/c", CAId: "ca"}, "client_key_path"},
{"missing ca_id", Config{APIUrl: "http://x", ClientCertPath: "/c", ClientKeyPath: "/k"}, "ca_id"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := New(nil, slog.Default())
raw, _ := json.Marshal(tc.cfg)
err := c.ValidateConfig(context.Background(), raw)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Errorf("expected error containing %q, got %v", tc.want, err)
}
})
}
}
func TestEntrust_ValidateConfig_BadCertPath(t *testing.T) {
c := New(nil, slog.Default())
cfg := Config{
APIUrl: "http://example.invalid",
ClientCertPath: "/nonexistent/cert.pem",
ClientKeyPath: "/nonexistent/key.pem",
CAId: "ca-1",
}
raw, _ := json.Marshal(cfg)
err := c.ValidateConfig(context.Background(), raw)
if err == nil || !strings.Contains(err.Error(), "mTLS credentials") {
t.Errorf("expected mTLS credentials error, got %v", err)
}
}
// ─────────────────────────────────────────────────────────────────────────────
// GetOrderStatus: 403 / malformed JSON / unknown status / pending happy path
// ─────────────────────────────────────────────────────────────────────────────
func TestEntrust_GetOrderStatus_403(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
}))
defer srv.Close()
c := buildEntrustConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "tracking-id")
if err == nil || !strings.Contains(err.Error(), "403") {
t.Errorf("expected 403 error, got %v", err)
}
}
func TestEntrust_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 := buildEntrustConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "tracking-id")
if err == nil || !strings.Contains(err.Error(), "parse") {
t.Errorf("expected parse error, got %v", err)
}
}
func TestEntrust_GetOrderStatus_StatusVariants(t *testing.T) {
cases := []struct {
statusVal string
want string
}{
{"PENDING", "pending"},
{"PROCESSING", "pending"},
{"REJECTED", "failed"},
{"DENIED", "failed"},
{"FAILED", "failed"},
{"WeirdStatus", "pending"}, // unknown → default pending
}
for _, tc := range cases {
t.Run(tc.statusVal, func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"status": tc.statusVal,
"trackingId": "tid-1",
})
}))
defer srv.Close()
c := buildEntrustConnector(t, srv.URL)
st, err := c.GetOrderStatus(context.Background(), "tid-1")
if err != nil {
t.Fatalf("GetOrderStatus: %v", err)
}
if st.Status != tc.want {
t.Errorf("expected status=%q for input=%q, got %q", tc.want, tc.statusVal, st.Status)
}
})
}
}
@@ -0,0 +1,158 @@
package globalsign_test
import (
"context"
"crypto/tls"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/shankar0123/certctl/internal/connector/issuer/globalsign"
)
// Bundle N.A/B-extended: globalsign failure-mode round-out (78.2% → ≥85%).
// Targets uncovered branches in getHTTPClient / GetOrderStatus / parseCertDates.
func buildGlobalsignConnector(t *testing.T, baseURL string) *globalsign.Connector {
t.Helper()
cfg := &globalsign.Config{
APIUrl: baseURL,
APIKey: "k",
APISecret: "s",
}
// Use NewWithHTTPClient with a test client so getHTTPClient short-circuits
// (no mTLS cert loading). Custom transport is required so the
// `httpClient.Transport != nil` test-mode check fires.
httpClient := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}} //nolint:gosec
return globalsign.NewWithHTTPClient(cfg, slog.Default(), httpClient)
}
func TestGlobalsign_GetOrderStatus_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":"unauthorized"}`))
}))
defer srv.Close()
c := buildGlobalsignConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "serial-123")
if err == nil || !strings.Contains(err.Error(), "403") {
t.Errorf("expected 403 error, got %v", err)
}
}
func TestGlobalsign_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 := buildGlobalsignConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "serial-123")
if err == nil || !strings.Contains(err.Error(), "parse") {
t.Errorf("expected parse error, got %v", err)
}
}
func TestGlobalsign_GetOrderStatus_StatusVariants(t *testing.T) {
cases := []struct {
statusVal string
want string
}{
{"pending", "pending"},
{"processing", "pending"},
{"rejected", "failed"},
{"denied", "failed"},
{"failed", "failed"},
{"weird-new-status", "pending"}, // unknown → default pending
}
for _, tc := range cases {
t.Run(tc.statusVal, func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"status": tc.statusVal,
"serial_number": "serial-123",
})
}))
defer srv.Close()
c := buildGlobalsignConnector(t, srv.URL)
st, err := c.GetOrderStatus(context.Background(), "serial-123")
if err != nil {
t.Fatalf("GetOrderStatus: %v", err)
}
if st.Status != tc.want {
t.Errorf("expected status=%q for input=%q, got %q", tc.want, tc.statusVal, st.Status)
}
})
}
}
func TestGlobalsign_GetOrderStatus_IssuedButCertMissing(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"issued","certificate":""}`))
}))
defer srv.Close()
c := buildGlobalsignConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "serial-123")
if err == nil || !strings.Contains(err.Error(), "certificate PEM is missing") {
t.Errorf("expected 'certificate PEM is missing' error, got %v", err)
}
}
func TestGlobalsign_GetOrderStatus_IssuedWithMalformedPEM_NonFatalParseDateWarning(t *testing.T) {
// When status=issued and certificate is non-empty but doesn't parse as PEM,
// the connector logs a warning but still returns Status=completed (per the
// existing code: parseCertDates failure is non-fatal).
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"issued","certificate":"not-a-pem-block","serial_number":"sn1"}`))
}))
defer srv.Close()
c := buildGlobalsignConnector(t, srv.URL)
st, err := c.GetOrderStatus(context.Background(), "serial-123")
if err != nil {
t.Fatalf("GetOrderStatus: %v", err)
}
if st.Status != "completed" {
t.Errorf("expected completed (parseCertDates failure is non-fatal), got %q", st.Status)
}
}
func TestGlobalsign_GetHTTPClient_NoMTLSCertPaths_ReturnsClientAsIs(t *testing.T) {
// When ClientCertPath and ClientKeyPath are both empty, getHTTPClient
// returns httpClient as-is — exercises that branch.
cfg := &globalsign.Config{
APIUrl: "http://example.invalid",
APIKey: "k",
APISecret: "s",
// no cert paths
}
c := globalsign.NewWithHTTPClient(cfg, slog.Default(), &http.Client{})
// GetOrderStatus will fail at HTTP do (invalid host), but getHTTPClient
// will have been exercised through the no-mTLS branch.
_, err := c.GetOrderStatus(context.Background(), "x")
if err == nil {
t.Errorf("expected error from invalid host")
}
}
func TestGlobalsign_GetHTTPClient_MTLSPathConfigured_LoadsKeyPair(t *testing.T) {
// Configure cert paths to a non-existent file — exercises the
// LoadX509KeyPair error branch in getHTTPClient.
cfg := &globalsign.Config{
APIUrl: "http://example.invalid",
APIKey: "k",
APISecret: "s",
ClientCertPath: "/nonexistent/cert.pem",
ClientKeyPath: "/nonexistent/key.pem",
}
c := globalsign.New(cfg, slog.Default())
_, err := c.GetOrderStatus(context.Background(), "x")
if err == nil || !strings.Contains(err.Error(), "client certificate") {
t.Errorf("expected 'client certificate' load error, got %v", err)
}
}
@@ -0,0 +1,195 @@
package sectigo_test
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/shankar0123/certctl/internal/connector/issuer/sectigo"
)
// Bundle N.A/B-extended: sectigo failure-mode round-out (79.4% → ≥85%).
// Targets uncovered branches in IssueCertificate / GetOrderStatus /
// checkStatus / collectCertificate / parsePEMBundle.
func buildSectigoConnector(t *testing.T, baseURL string) *sectigo.Connector {
t.Helper()
c := sectigo.New(nil, slog.Default())
cfg := sectigo.Config{
BaseURL: baseURL,
CustomerURI: "tcust",
Login: "user",
Password: "pw",
CertType: 1,
OrgID: 2,
Term: 365,
}
raw, _ := json.Marshal(cfg)
if err := c.ValidateConfig(context.Background(), raw); err != nil {
t.Fatalf("ValidateConfig: %v", err)
}
return c
}
// Sectigo's ValidateConfig hits /ssl/v1/types — need a valid response.
func sectigoValidateOK(w http.ResponseWriter) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`[{"id":1,"name":"InstantSSL"}]`))
}
func TestSectigo_GetOrderStatus_InvalidSslId(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/ssl/v1/types" {
sectigoValidateOK(w)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
c := buildSectigoConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "not-a-number")
if err == nil || !strings.Contains(err.Error(), "invalid") {
t.Errorf("expected 'invalid Sectigo ssl_id' error, got %v", err)
}
}
func TestSectigo_CheckStatus_404_ReturnsError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/ssl/v1/types" {
sectigoValidateOK(w)
return
}
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"description":"not found"}`))
}))
defer srv.Close()
c := buildSectigoConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "999")
if err == nil || !strings.Contains(err.Error(), "404") {
t.Errorf("expected 404 status error, got %v", err)
}
}
func TestSectigo_CheckStatus_MalformedJSON(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/ssl/v1/types" {
sectigoValidateOK(w)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{not json`))
}))
defer srv.Close()
c := buildSectigoConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "100")
if err == nil || !strings.Contains(err.Error(), "parse") {
t.Errorf("expected parse error, got %v", err)
}
}
func TestSectigo_GetOrderStatus_AppliedAndPending(t *testing.T) {
cases := []struct {
statusVal string
want string
}{
{"Applied", "pending"},
{"Pending", "pending"},
{"Rejected", "failed"},
{"Revoked", "failed"},
{"Expired", "failed"},
{"Not Enrolled", "failed"},
{"WeirdNewStatus", "pending"}, // unknown → default pending
}
for _, tc := range cases {
t.Run(tc.statusVal, func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/ssl/v1/types" {
sectigoValidateOK(w)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"` + tc.statusVal + `"}`))
}))
defer srv.Close()
c := buildSectigoConnector(t, srv.URL)
st, err := c.GetOrderStatus(context.Background(), "55001")
if err != nil {
t.Fatalf("GetOrderStatus: %v", err)
}
if st.Status != tc.want {
t.Errorf("expected status=%q, got %q", tc.want, st.Status)
}
})
}
}
func TestSectigo_CollectCertificate_BadRequest_TreatedAsPending(t *testing.T) {
// Sectigo returns 400 with code -183 when cert approved but not yet generated.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/ssl/v1/types":
sectigoValidateOK(w)
case strings.HasPrefix(r.URL.Path, "/ssl/v1/collect/"):
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"code":-183,"description":"certificate not yet ready"}`))
default:
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"Issued"}`))
}
}))
defer srv.Close()
c := buildSectigoConnector(t, srv.URL)
st, err := c.GetOrderStatus(context.Background(), "55001")
if err != nil {
t.Fatalf("GetOrderStatus: %v", err)
}
if st.Status != "pending" {
t.Errorf("expected pending (cert not yet ready), got %q", st.Status)
}
}
func TestSectigo_CollectCertificate_500_PropagatesError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/ssl/v1/types":
sectigoValidateOK(w)
case strings.HasPrefix(r.URL.Path, "/ssl/v1/collect/"):
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`internal error`))
default:
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"Issued"}`))
}
}))
defer srv.Close()
c := buildSectigoConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "55001")
if err == nil || !strings.Contains(err.Error(), "500") {
t.Errorf("expected 500 error, got %v", err)
}
}
func TestSectigo_CollectCertificate_MalformedPEM_FailsClean(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/ssl/v1/types":
sectigoValidateOK(w)
case strings.HasPrefix(r.URL.Path, "/ssl/v1/collect/"):
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("not a pem"))
default:
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"Issued"}`))
}
}))
defer srv.Close()
c := buildSectigoConnector(t, srv.URL)
_, err := c.GetOrderStatus(context.Background(), "55001")
if err == nil {
t.Errorf("expected error from malformed PEM bundle")
}
}
@@ -0,0 +1,148 @@
package vault_test
import (
"context"
"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/vault"
)
// 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: "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
}