package sectigo_test import ( "context" "encoding/json" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "github.com/certctl-io/certctl/internal/connector/issuer/sectigo" "github.com/certctl-io/certctl/internal/secret" ) // 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: secret.NewRefFromString("user"), Password: secret.NewRefFromString("pw"), CertType: 1, OrgID: 2, Term: 365, PollMaxWaitSeconds: 1, // keep async-pending tests fast } 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") } }