package entrust_test import ( "context" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/json" "encoding/pem" "fmt" "log/slog" "math/big" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "github.com/certctl-io/certctl/internal/connector/issuer" "github.com/certctl-io/certctl/internal/connector/issuer/entrust" ) func TestEntrustConnector(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) ctx := context.Background() t.Run("ValidateConfig_Success", func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v1/certificate-authorities/ca-test-123" { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"caId":"ca-test-123","name":"Test CA"}`)) return } http.NotFound(w, r) })) defer srv.Close() config := entrust.Config{ APIUrl: srv.URL, ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-test-123", } connector := entrust.New(nil, logger) rawConfig, _ := json.Marshal(config) // ValidateConfig will fail due to invalid cert paths, but we're testing the logic flow // In real usage, valid cert files would be provided err := connector.ValidateConfig(ctx, rawConfig) // We expect an error due to invalid cert paths, which is normal if err != nil && !strings.Contains(err.Error(), "load mTLS") { // Some other error occurred that we're not expecting t.Logf("Got expected error for invalid cert paths: %v", err) } }) t.Run("ValidateConfig_MissingAPIUrl", func(t *testing.T) { config := entrust.Config{ ClientCertPath: "/path/to/cert", ClientKeyPath: "/path/to/key", CAId: "ca-123", } connector := entrust.New(nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for missing api_url") } if !strings.Contains(err.Error(), "api_url is required") { t.Errorf("Expected api_url required error, got: %v", err) } }) t.Run("ValidateConfig_MissingClientCertPath", func(t *testing.T) { config := entrust.Config{ APIUrl: "https://api.entrust.com", ClientKeyPath: "/path/to/key", CAId: "ca-123", } connector := entrust.New(nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for missing client_cert_path") } if !strings.Contains(err.Error(), "client_cert_path is required") { t.Errorf("Expected client_cert_path required error, got: %v", err) } }) t.Run("ValidateConfig_MissingClientKeyPath", func(t *testing.T) { config := entrust.Config{ APIUrl: "https://api.entrust.com", ClientCertPath: "/path/to/cert", CAId: "ca-123", } connector := entrust.New(nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for missing client_key_path") } if !strings.Contains(err.Error(), "client_key_path is required") { t.Errorf("Expected client_key_path required error, got: %v", err) } }) t.Run("ValidateConfig_MissingCAId", func(t *testing.T) { config := entrust.Config{ APIUrl: "https://api.entrust.com", ClientCertPath: "/path/to/cert", ClientKeyPath: "/path/to/key", } connector := entrust.New(nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for missing ca_id") } if !strings.Contains(err.Error(), "ca_id is required") { t.Errorf("Expected ca_id required error, got: %v", err) } }) t.Run("IssueCertificate_Synchronous", func(t *testing.T) { testCertPEM, _ := generateTestCert(t) testChainPEM, _ := generateTestCert(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/enrollments") && r.Method == http.MethodPost { w.WriteHeader(http.StatusOK) w.Write([]byte(fmt.Sprintf(`{"trackingId":"ENR-2024-001","status":"ISSUED","certificate":"%s","chain":"%s"}`, escapeJSON(testCertPEM), escapeJSON(testChainPEM)))) return } http.NotFound(w, r) })) defer srv.Close() config := &entrust.Config{ APIUrl: srv.URL, ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-123", } connector := entrust.NewWithHTTPClient(config, logger, srv.Client()) _, csrPEM := generateTestCSR(t, "app.example.com") req := issuer.IssuanceRequest{ CommonName: "app.example.com", SANs: []string{"app.example.com"}, CSRPEM: csrPEM, } result, err := connector.IssueCertificate(ctx, req) if err != nil { t.Fatalf("IssueCertificate failed: %v", err) } if result.CertPEM == "" { t.Error("CertPEM should not be empty for immediate issuance") } if result.Serial == "" { t.Error("Serial should not be empty for immediate issuance") } if result.OrderID != "ENR-2024-001" { t.Errorf("Expected OrderID 'ENR-2024-001', got '%s'", result.OrderID) } t.Logf("Entrust issued cert: serial=%s, orderID=%s", result.Serial, result.OrderID) }) t.Run("IssueCertificate_AsyncPending", func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/enrollments") && r.Method == http.MethodPost { w.WriteHeader(http.StatusCreated) w.Write([]byte(`{"trackingId":"ENR-2024-002","status":"PENDING"}`)) return } http.NotFound(w, r) })) defer srv.Close() config := &entrust.Config{ APIUrl: srv.URL, ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-123", } connector := entrust.NewWithHTTPClient(config, logger, srv.Client()) _, csrPEM := generateTestCSR(t, "secure.example.com") req := issuer.IssuanceRequest{ CommonName: "secure.example.com", CSRPEM: csrPEM, } result, err := connector.IssueCertificate(ctx, req) if err != nil { t.Fatalf("IssueCertificate failed: %v", err) } if result.OrderID != "ENR-2024-002" { t.Errorf("Expected OrderID 'ENR-2024-002', got '%s'", result.OrderID) } if result.CertPEM != "" { t.Error("CertPEM should be empty for pending order") } if result.Serial != "" { t.Error("Serial should be empty for pending order") } }) t.Run("IssueCertificate_WithProfileId", func(t *testing.T) { testCertPEM, _ := generateTestCert(t) var receivedProfileId string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/enrollments") && r.Method == http.MethodPost { // Parse request to verify profileId was sent var req map[string]interface{} json.NewDecoder(r.Body).Decode(&req) if pid, ok := req["profileId"].(string); ok { receivedProfileId = pid } w.WriteHeader(http.StatusOK) w.Write([]byte(fmt.Sprintf(`{"trackingId":"ENR-2024-003","status":"ISSUED","certificate":"%s"}`, escapeJSON(testCertPEM)))) return } http.NotFound(w, r) })) defer srv.Close() config := &entrust.Config{ APIUrl: srv.URL, ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-123", ProfileId: "prof-ov-basic", } connector := entrust.NewWithHTTPClient(config, logger, srv.Client()) _, csrPEM := generateTestCSR(t, "app.example.com") req := issuer.IssuanceRequest{ CommonName: "app.example.com", CSRPEM: csrPEM, } result, err := connector.IssueCertificate(ctx, req) if err != nil { t.Fatalf("IssueCertificate failed: %v", err) } if result.OrderID == "" { t.Error("OrderID should not be empty") } if receivedProfileId != "prof-ov-basic" { t.Errorf("Expected profileId 'prof-ov-basic', got '%s'", receivedProfileId) } }) t.Run("IssueCertificate_ServerError", func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"invalid CSR format"}`)) })) defer srv.Close() config := &entrust.Config{ APIUrl: srv.URL, ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-123", } connector := entrust.NewWithHTTPClient(config, logger, srv.Client()) req := issuer.IssuanceRequest{ CommonName: "test.example.com", CSRPEM: "invalid-csr", } _, err := connector.IssueCertificate(ctx, req) if err == nil { t.Fatal("Expected error for server error response") } }) t.Run("GetOrderStatus_Issued", func(t *testing.T) { testCertPEM, _ := generateTestCert(t) testChainPEM, _ := generateTestCert(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/enrollments/ENR-2024-001") && r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) w.Write([]byte(fmt.Sprintf(`{"trackingId":"ENR-2024-001","status":"ISSUED","certificate":"%s","chain":"%s"}`, escapeJSON(testCertPEM), escapeJSON(testChainPEM)))) return } http.NotFound(w, r) })) defer srv.Close() config := &entrust.Config{ APIUrl: srv.URL, ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-123", } connector := entrust.NewWithHTTPClient(config, logger, srv.Client()) status, err := connector.GetOrderStatus(ctx, "ENR-2024-001") if err != nil { t.Fatalf("GetOrderStatus failed: %v", err) } if status.Status != "completed" { t.Errorf("Expected status 'completed', got '%s'", status.Status) } if status.CertPEM == nil || *status.CertPEM == "" { t.Error("CertPEM should not be empty for issued order") } if status.Serial == nil || *status.Serial == "" { t.Error("Serial should not be empty for issued order") } }) t.Run("GetOrderStatus_Pending", func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/enrollments/ENR-2024-002") { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"trackingId":"ENR-2024-002","status":"PENDING"}`)) return } http.NotFound(w, r) })) defer srv.Close() config := &entrust.Config{ APIUrl: srv.URL, ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-123", PollMaxWaitSeconds: 1, // keep async-pending tests fast } connector := entrust.NewWithHTTPClient(config, logger, srv.Client()) status, err := connector.GetOrderStatus(ctx, "ENR-2024-002") if err != nil { t.Fatalf("GetOrderStatus failed: %v", err) } if status.Status != "pending" { t.Errorf("Expected status 'pending', got '%s'", status.Status) } if status.CertPEM != nil { t.Error("CertPEM should be nil for pending order") } }) t.Run("GetOrderStatus_Failed", func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/enrollments/ENR-2024-003") { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"trackingId":"ENR-2024-003","status":"REJECTED"}`)) return } http.NotFound(w, r) })) defer srv.Close() config := &entrust.Config{ APIUrl: srv.URL, ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-123", } connector := entrust.NewWithHTTPClient(config, logger, srv.Client()) status, err := connector.GetOrderStatus(ctx, "ENR-2024-003") if err != nil { t.Fatalf("GetOrderStatus failed: %v", err) } if status.Status != "failed" { t.Errorf("Expected status 'failed', got '%s'", status.Status) } }) t.Run("RenewCertificate_Success", func(t *testing.T) { testCertPEM, _ := generateTestCert(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/enrollments") && r.Method == http.MethodPost { w.WriteHeader(http.StatusCreated) w.Write([]byte(fmt.Sprintf(`{"trackingId":"ENR-2024-010","status":"ISSUED","certificate":"%s"}`, escapeJSON(testCertPEM)))) return } http.NotFound(w, r) })) defer srv.Close() config := &entrust.Config{ APIUrl: srv.URL, ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-123", } connector := entrust.NewWithHTTPClient(config, logger, srv.Client()) _, csrPEM := generateTestCSR(t, "renew.example.com") renewReq := issuer.RenewalRequest{ CommonName: "renew.example.com", CSRPEM: csrPEM, } result, err := connector.RenewCertificate(ctx, renewReq) if err != nil { t.Fatalf("RenewCertificate failed: %v", err) } if result.OrderID == "" { t.Error("OrderID should not be empty") } if result.Serial == "" { t.Error("Serial should not be empty for immediate renewal") } }) t.Run("RevokeCertificate_Success", func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/certificates/") && strings.Contains(r.URL.Path, "/revoke") && r.Method == http.MethodPut { w.WriteHeader(http.StatusNoContent) return } http.NotFound(w, r) })) defer srv.Close() config := &entrust.Config{ APIUrl: srv.URL, ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-123", } connector := entrust.NewWithHTTPClient(config, logger, srv.Client()) reason := "keyCompromise" revokeReq := issuer.RevocationRequest{ Serial: "88001", Reason: &reason, } err := connector.RevokeCertificate(ctx, revokeReq) if err != nil { t.Fatalf("RevokeCertificate failed: %v", err) } }) t.Run("RevokeCertificate_Error", func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":"certificate not found"}`)) })) defer srv.Close() config := &entrust.Config{ APIUrl: srv.URL, ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-123", } connector := entrust.NewWithHTTPClient(config, logger, srv.Client()) revokeReq := issuer.RevocationRequest{ Serial: "00000", } err := connector.RevokeCertificate(ctx, revokeReq) if err == nil { t.Fatal("Expected error for revocation of nonexistent cert") } }) t.Run("GetCACertPEM_Error", func(t *testing.T) { config := &entrust.Config{ APIUrl: "https://api.entrust.com", ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-123", } connector := entrust.New(config, logger) _, err := connector.GetCACertPEM(ctx) if err == nil { t.Fatal("GetCACertPEM should return error for Entrust") } }) t.Run("GetRenewalInfo_ReturnsNil", func(t *testing.T) { config := &entrust.Config{ APIUrl: "https://api.entrust.com", ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-123", } connector := entrust.New(config, logger) result, err := connector.GetRenewalInfo(ctx, "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----") if err != nil { t.Fatalf("GetRenewalInfo should not return error, got: %v", err) } if result != nil { t.Fatal("GetRenewalInfo should return nil for Entrust") } }) t.Run("GenerateCRL_Error", func(t *testing.T) { config := &entrust.Config{ APIUrl: "https://api.entrust.com", ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-123", } connector := entrust.New(config, logger) _, err := connector.GenerateCRL(ctx, []issuer.RevokedCertEntry{}) if err == nil { t.Fatal("GenerateCRL should return error for Entrust") } }) t.Run("SignOCSPResponse_Error", func(t *testing.T) { config := &entrust.Config{ APIUrl: "https://api.entrust.com", ClientCertPath: "/dev/null", ClientKeyPath: "/dev/null", CAId: "ca-123", } connector := entrust.New(config, logger) _, err := connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{}) if err == nil { t.Fatal("SignOCSPResponse should return error for Entrust") } }) } // Helper functions // generateTestCert creates a self-signed test certificate and returns the PEM string. func generateTestCert(t *testing.T) (certPEM string, keyPEM string) { t.Helper() key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("Failed to generate key: %v", err) } serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) template := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{ CommonName: fmt.Sprintf("Test Certificate %s", serial.String()[:8]), }, DNSNames: []string{"test.example.com"}, KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, NotBefore: time.Now(), NotAfter: time.Now().AddDate(1, 0, 0), } certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) if err != nil { t.Fatalf("Failed to create certificate: %v", err) } certPEM = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})) keyPEM = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})) return certPEM, keyPEM } // generateTestCSR creates a test CSR for the given common name. func generateTestCSR(t *testing.T, commonName string) (*x509.CertificateRequest, string) { t.Helper() key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("Failed to generate key: %v", err) } csrTemplate := x509.CertificateRequest{ Subject: pkix.Name{ CommonName: commonName, }, DNSNames: []string{commonName}, SignatureAlgorithm: x509.SHA256WithRSA, } csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, key) if err != nil { t.Fatalf("Failed to create CSR: %v", err) } csrPEM := string(pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE REQUEST", Bytes: csrBytes, })) csr, err := x509.ParseCertificateRequest(csrBytes) if err != nil { t.Fatalf("Failed to parse CSR: %v", err) } return csr, csrPEM } // escapeJSON escapes special characters in a string for safe JSON embedding. func escapeJSON(s string) string { // Replace newlines and quotes for safe JSON embedding s = strings.ReplaceAll(s, "\n", "\\n") s = strings.ReplaceAll(s, "\"", "\\\"") return s } // Ensure NewWithHTTPClient is properly exported for testing. // This function is required to be exported for tests to work. func init() { // Ensure tls package is imported for any mTLS setup _ = tls.Certificate{} }