mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 18:51:32 +00:00
Merge fix/coverage-N.AB-extended: Bundle N.A/B-extended — 6 connectors lifted; M-001 closed
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user