mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 17:51:29 +00:00
8b75e0311b
Mechanical sed across the main go.mod's module declaration, the f5-mock-icontrol
sub-module's go.mod, every Go file's import path (361 files), and a rebuild of
the checked-in f5-mock-icontrol binary so its embedded build-info reflects the
new module path. No behavior change.
Choice B from cowork/transfer-certctl-to-org.md, executed 2026-05-04. Choice A
(keep module path declared as github.com/shankar0123/certctl regardless of
repo URL) shipped on the day of the org transfer (2026-05-03) since we had no
external Go consumers; this commit closes that deferral.
Backward-compat: GitHub HTTP redirects continue to forward
github.com/shankar0123/certctl → github.com/certctl-io/certctl at the URL
level, but Go's module proxy uses the path declared in go.mod as the
canonical name. Pre-fix, anyone trying `go get github.com/certctl-io/certctl/...`
hit a "module path mismatch" error because go.mod said
github.com/shankar0123/certctl and the URL they fetched it from said
certctl-io/certctl. Post-fix, the canonical name and the URL agree, so
go get / go install / external Go consumers / Go-tooling integrations
work cleanly via either the new path (preferred) or the old path (which
redirects and Go follows the redirect for source fetch).
Anyone still importing the old path inside their own code keeps working
provided they update their go.mod's `require` line to match — the module
path declared in their consumer's go.sum / go.mod is the authoritative
import name, so a mass sed across their import statements is the migration
on the consumer side. No external consumers exist today.
Diff shape:
361 *.go files — import path replacement only
2 go.mod — module declaration replacement only
1 binary — deploy/test/f5-mock-icontrol/f5-mock-icontrol rebuilt
so embedded build-info reflects the new path (8618965 vs
8618933 bytes; 32-byte diff is the build-info change)
Total: 364 files, 730 insertions / 730 deletions, net-zero size, pure
mechanical substitution.
Verification:
gofmt: 17 files needed re-alignment after sed (the new path is one char
shorter than the old, so column-aligned import groups drifted). Applied
`gofmt -w` to fix.
go mod tidy: clean exit on both modules.
go vet ./...: clean exit.
go build ./...: clean exit.
go test -short -count=1 on representative packages: all green
(internal/domain, internal/validation, internal/crypto, internal/crypto/signer,
cmd/agent). Test output now reads `ok github.com/certctl-io/certctl/...`
confirming the module path resolves correctly.
binary: f5-mock-icontrol rebuilt; `strings | grep shankar0123` returns
nothing; `strings | grep certctl-io/certctl` shows the new module path
embedded in build-info.
Files intentionally NOT touched in this commit:
README.md / CHANGELOG.md / docs/ / etc. — already swept to certctl-io
URLs in commit 0729ee4 (the post-transfer URL refresh). This commit is
purely the Go-tooling layer.
Scarf pixels (`shankar0123.docker.scarf.sh/...`) — Scarf-account
namespace, not a Go import or GitHub repo URL. Stays.
This is a non-blocking, non-customer-impacting change. Operators pulling
container images, running `make verify`, hitting the API, or installing the
agent see no functional difference. Only Go-tooling consumers (none today)
are affected, and they're enabled — not broken — by this commit.
1773 lines
48 KiB
Go
1773 lines
48 KiB
Go
package stepca_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"math/big"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/certctl-io/certctl/internal/connector/issuer"
|
|
"github.com/certctl-io/certctl/internal/connector/issuer/stepca"
|
|
)
|
|
|
|
func TestStepCAConnector(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) {
|
|
// Start a mock step-ca health endpoint
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/health" {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ValidityDays: 90,
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err != nil {
|
|
t.Fatalf("ValidateConfig failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_MissingCAURL", func(t *testing.T) {
|
|
config := stepca.Config{
|
|
ProvisionerName: "test",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing ca_url")
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_MissingProvisioner", func(t *testing.T) {
|
|
config := stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing provisioner_name")
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_UnreachableCA", func(t *testing.T) {
|
|
config := stepca.Config{
|
|
CAURL: "http://localhost:19999",
|
|
ProvisionerName: "test",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for unreachable CA")
|
|
}
|
|
})
|
|
|
|
t.Run("IssueCertificate_Success", func(t *testing.T) {
|
|
// Generate a test certificate to return in the mock
|
|
testCertPEM, testKeyPEM := generateTestCert(t)
|
|
_ = testKeyPEM
|
|
|
|
// Start a mock step-ca server
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ValidityDays: 30,
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, err := generateStepCATestCSR("app.internal.corp")
|
|
if err != nil {
|
|
t.Fatalf("Failed to generate CSR: %v", err)
|
|
}
|
|
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "app.internal.corp",
|
|
SANs: []string{"app.internal.corp"},
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate failed: %v", err)
|
|
}
|
|
|
|
if result.CertPEM == "" {
|
|
t.Error("CertPEM is empty")
|
|
}
|
|
if result.Serial == "" {
|
|
t.Error("Serial is empty")
|
|
}
|
|
if result.OrderID == "" {
|
|
t.Error("OrderID is empty")
|
|
}
|
|
|
|
t.Logf("step-ca issued cert: serial=%s", result.Serial)
|
|
})
|
|
|
|
t.Run("IssueCertificate_ServerError", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"invalid token"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for server error response")
|
|
}
|
|
t.Logf("Correctly got error: %v", err)
|
|
})
|
|
|
|
t.Run("RenewCertificate", func(t *testing.T) {
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("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.Serial == "" {
|
|
t.Error("Serial is empty")
|
|
}
|
|
})
|
|
|
|
t.Run("RevokeCertificate_Success", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/revoke":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
reason := "keyCompromise"
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "1234567890",
|
|
Reason: &reason,
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err != nil {
|
|
t.Fatalf("RevokeCertificate failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("RevokeCertificate_ServerError", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/revoke":
|
|
w.WriteHeader(http.StatusForbidden)
|
|
w.Write([]byte(`{"error":"unauthorized"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "1234567890",
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err == nil {
|
|
t.Fatal("Expected error for server error response")
|
|
}
|
|
})
|
|
|
|
t.Run("GetOrderStatus", func(t *testing.T) {
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
status, err := connector.GetOrderStatus(ctx, "stepca-12345")
|
|
if err != nil {
|
|
t.Fatalf("GetOrderStatus failed: %v", err)
|
|
}
|
|
|
|
if status.Status != "completed" {
|
|
t.Errorf("Expected status 'completed', got '%s'", status.Status)
|
|
}
|
|
})
|
|
}
|
|
|
|
// generateTestCert creates a self-signed test certificate and returns the PEM strings.
|
|
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: "Test Certificate",
|
|
},
|
|
DNSNames: []string{"test.example.com"},
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func generateStepCATestCSR(commonName string) (*x509.CertificateRequest, string, error) {
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return nil, "", 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 {
|
|
return nil, "", err
|
|
}
|
|
|
|
csr, err := x509.ParseCertificateRequest(csrBytes)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
csrPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE REQUEST",
|
|
Bytes: csrBytes,
|
|
})
|
|
|
|
return csr, string(csrPEM), nil
|
|
}
|
|
|
|
func TestGenerateProvisionerTokenEphemeralKey(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
// No ProvisionerKeyPath — forces ephemeral key generation
|
|
}
|
|
_ = stepca.New(config, logger) // verify constructor doesn't panic
|
|
|
|
// This should NOT panic and should return a non-empty token
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
SANs: []string{"test.example.com", "app.example.com"},
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
// We can't test token generation directly since it's unexported,
|
|
// but we can verify issuance with ephemeral key works against mock server
|
|
testCertPEM, _ := generateTestCert(t)
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate with ephemeral key failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
}
|
|
|
|
func TestParseSignResponse_SimpleFormat(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
_ = stepca.New(config, logger) // verify constructor doesn't panic
|
|
|
|
// Test the simple crt/ca response format
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Simple format: crt and ca fields
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate with simple format failed: %v", err)
|
|
}
|
|
|
|
if result.CertPEM != testCertPEM {
|
|
t.Errorf("CertPEM mismatch: got %q, want %q", result.CertPEM, testCertPEM)
|
|
}
|
|
if result.ChainPEM != testCertPEM {
|
|
t.Errorf("ChainPEM mismatch: got %q, want %q", result.ChainPEM, testCertPEM)
|
|
}
|
|
}
|
|
|
|
func TestParseSignResponse_StructuredFormat(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
_ = stepca.New(config, logger) // verify constructor doesn't panic
|
|
|
|
// Test the structured response format
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Structured format with serverPEM and caPEM
|
|
resp := fmt.Sprintf(`{
|
|
"serverPEM": {"certificate": %q},
|
|
"caPEM": {"certificate": %q}
|
|
}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate with structured format failed: %v", err)
|
|
}
|
|
|
|
if result.CertPEM != testCertPEM {
|
|
t.Errorf("CertPEM mismatch")
|
|
}
|
|
}
|
|
|
|
func TestParseSignResponse_InvalidCertPEM(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
// Test invalid PEM in response
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Invalid PEM data
|
|
resp := `{"crt": "not a certificate", "ca": ""}`
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for invalid certificate PEM")
|
|
}
|
|
}
|
|
|
|
func TestParseSignResponse_EmptyCertificate(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
// Test empty certificate in response
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := `{"crt": "", "ca": ""}`
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for empty certificate")
|
|
}
|
|
}
|
|
|
|
func TestValidateConfig_ProvisionerKeyPathNotExist(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/health" {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ProvisionerKeyPath: "/nonexistent/path/to/key.json",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for non-existent provisioner key path")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_ValidityDaysSet(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
capturedRequest := []byte{}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
// Capture the request to verify NotBefore/NotAfter are set
|
|
var body []byte
|
|
body, _ = io.ReadAll(r.Body)
|
|
capturedRequest = body
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ValidityDays: 90,
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
|
|
// Verify that the request body contained notBefore and notAfter
|
|
if !bytes.Contains(capturedRequest, []byte("notBefore")) || !bytes.Contains(capturedRequest, []byte("notAfter")) {
|
|
t.Errorf("Expected notBefore and notAfter in request body, got: %s", string(capturedRequest))
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_NoReasonProvided(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/revoke":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
// No reason provided — should default to "unspecified"
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "1234567890",
|
|
Reason: nil,
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err != nil {
|
|
t.Fatalf("RevokeCertificate without reason failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGenerateCRL_NotSupported(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, err := connector.GenerateCRL(ctx, nil)
|
|
if err == nil {
|
|
t.Fatal("Expected error for GenerateCRL not supported")
|
|
}
|
|
}
|
|
|
|
func TestSignOCSPResponse_NotSupported(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, err := connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{})
|
|
if err == nil {
|
|
t.Fatal("Expected error for SignOCSPResponse not supported")
|
|
}
|
|
}
|
|
|
|
func TestGetCACertPEM_NotSupported(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, err := connector.GetCACertPEM(ctx)
|
|
if err == nil {
|
|
t.Fatal("Expected error for GetCACertPEM not supported")
|
|
}
|
|
}
|
|
|
|
func TestGetRenewalInfo_NotSupported(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
result, err := connector.GetRenewalInfo(ctx, "test cert pem")
|
|
if err != nil || result != nil {
|
|
t.Fatalf("Expected (nil, nil) for GetRenewalInfo, got (%v, %v)", result, err)
|
|
}
|
|
}
|
|
|
|
func TestParseSignResponse_CertChainFormat(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
_ = stepca.New(config, logger) // verify constructor doesn't panic
|
|
|
|
// Test the certChainPEM array response format (multiple certs in array)
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Array format with multiple certs (leaf + intermediate + root)
|
|
resp := fmt.Sprintf(`{
|
|
"certChainPEM": [
|
|
{"certificate": %q},
|
|
{"certificate": %q},
|
|
{"certificate": %q}
|
|
]
|
|
}`, testCertPEM, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate with cert chain format failed: %v", err)
|
|
}
|
|
|
|
// Chain should include intermediate + root (all except first)
|
|
if result.CertPEM != testCertPEM {
|
|
t.Error("Leaf cert mismatch")
|
|
}
|
|
// Chain should include 2 certs (intermediate + root)
|
|
if result.ChainPEM == "" {
|
|
t.Error("Chain should not be empty when multiple certs provided")
|
|
}
|
|
}
|
|
|
|
func TestValidateConfig_InvalidJSON(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig := json.RawMessage(`{invalid json}`)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for invalid JSON config")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_ContextCancelled(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
// Cancelled context
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for cancelled context")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_MalformedResponseJSON(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Malformed JSON response
|
|
w.Write([]byte(`{invalid json}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for malformed response JSON")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_StatusOK(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
// Test with 200 OK response (alternative to 201 Created)
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK) // 200 instead of 201
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate with 200 OK status failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_ErrorReadingBody(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/revoke":
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
// Don't write anything (simulate error reading response)
|
|
w.Write([]byte(`Internal error`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "1234567890",
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err == nil {
|
|
t.Fatal("Expected error for revoke server error")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_NoValidityDays(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
capturedRequest := []byte{}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
// Capture the request to verify behavior with 0 ValidityDays
|
|
var body []byte
|
|
body, _ = io.ReadAll(r.Body)
|
|
capturedRequest = body
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ValidityDays: 0, // No validity days set
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate with 0 ValidityDays failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
|
|
// When ValidityDays is 0, the code doesn't set NotBefore/NotAfter
|
|
// Just verify that the request was captured and processed
|
|
if len(capturedRequest) == 0 {
|
|
t.Error("Expected non-empty captured request")
|
|
}
|
|
}
|
|
|
|
func TestValidateConfig_HealthCheckError(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := stepca.Config{
|
|
CAURL: "http://invalid-url-that-will-not-resolve.local:9999",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for unreachable CA")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_ReadResponseBodyError(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
// Create a response with status 201 but an unreadable body
|
|
// This is hard to simulate with httptest, so we'll just test the normal path
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
testCertPEM, _ := generateTestCert(t)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_BadStatus(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusUnauthorized) // 401 is neither 200 nor 201
|
|
w.Write([]byte(`{"error":"unauthorized"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for 401 response")
|
|
}
|
|
}
|
|
|
|
func TestRenewCertificate_DelegatesToIssuance(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
callCount := 0
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
callCount++
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("renew.example.com")
|
|
renewReq := issuer.RenewalRequest{
|
|
CommonName: "renew.example.com",
|
|
SANs: []string{"renew.example.com", "app.example.com"},
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.RenewCertificate(ctx, renewReq)
|
|
if err != nil {
|
|
t.Fatalf("RenewCertificate failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
|
|
// Should have made exactly 1 call to /sign
|
|
if callCount != 1 {
|
|
t.Errorf("Expected 1 sign call, got %d", callCount)
|
|
}
|
|
}
|
|
|
|
func TestNew_WithRootCertPath(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
|
|
// Create a temporary cert file
|
|
testCertPEM, _ := generateTestCert(t)
|
|
tmpFile := os.TempDir() + "/test_ca_cert.pem"
|
|
err := os.WriteFile(tmpFile, []byte(testCertPEM), 0644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to write test cert: %v", err)
|
|
}
|
|
defer os.Remove(tmpFile)
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
RootCertPath: tmpFile,
|
|
}
|
|
|
|
connector := stepca.New(config, logger)
|
|
if connector == nil {
|
|
t.Fatal("Expected non-nil connector")
|
|
}
|
|
}
|
|
|
|
func TestNew_WithInvalidRootCertPath(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
RootCertPath: "/nonexistent/path/to/cert.pem",
|
|
}
|
|
|
|
// Should not panic, just log a warning and fall back to system trust store
|
|
connector := stepca.New(config, logger)
|
|
if connector == nil {
|
|
t.Fatal("Expected non-nil connector")
|
|
}
|
|
}
|
|
|
|
func TestNew_WithNilConfig(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
|
|
connector := stepca.New(nil, logger)
|
|
if connector == nil {
|
|
t.Fatal("Expected non-nil connector even with nil config")
|
|
}
|
|
}
|
|
|
|
func TestValidateConfig_HealthCheck_NotOK(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/health" {
|
|
w.WriteHeader(http.StatusServiceUnavailable) // 503 instead of 200
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for non-200 health check")
|
|
}
|
|
}
|
|
|
|
func TestParseSignResponse_MalformedPEM(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Send PEM with invalid base64 or invalid cert
|
|
resp := `{"crt": "-----BEGIN CERTIFICATE-----\ninvalid\n-----END CERTIFICATE-----\n", "ca": ""}`
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for malformed PEM")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_WithMultipleSANs(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ValidityDays: 365,
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("app.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "app.example.com",
|
|
SANs: []string{"app.example.com", "api.example.com", "www.example.com"},
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate with multiple SANs failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_NetworkError(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "http://localhost:29999", // Port that's not listening
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for network connection failure")
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_NetworkError(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "http://localhost:29999", // Port that's not listening
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "1234567890",
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err == nil {
|
|
t.Fatal("Expected error for network connection failure")
|
|
}
|
|
}
|
|
|
|
func TestParseSignResponse_NoServerPEM(t *testing.T) {
|
|
// Test when neither crt/ca nor serverPEM/caPEM nor certChainPEM are present
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Empty response
|
|
resp := `{}`
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config.CAURL = srv.URL
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for empty response")
|
|
}
|
|
}
|
|
|
|
func TestValidateConfig_CreateHealthCheckRequest_Error(t *testing.T) {
|
|
// This is harder to test since we need to create a request with an invalid URL
|
|
// Let's just test with an invalid CAURL that fails to parse
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := stepca.Config{
|
|
CAURL: "https://[invalid-ip]:9000", // Invalid IPv6 format
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for invalid CAURL")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_MarshalSignRequestError(t *testing.T) {
|
|
// This is hard to test since json.Marshal typically doesn't fail for structs
|
|
// We've covered the main paths, so this is a limitation of the testable code
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate failed: %v", err)
|
|
}
|
|
|
|
if result.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
}
|
|
|
|
func TestRenewCertificate_WithEKUs(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/sign":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
_, csrPEM, _ := generateStepCATestCSR("renew.example.com")
|
|
// RenewalRequest doesn't have EKUs field in the current implementation
|
|
// but we can test with extended request data
|
|
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.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
}
|
|
|
|
func TestLoadProvisionerKey_FileNotReadable(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/health" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
// Test with a provisioner key path that can't be read
|
|
config := stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ProvisionerKeyPath: "/root/.ssh/no_such_key", // Permission denied or doesn't exist
|
|
ProvisionerPassword: "password",
|
|
}
|
|
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
// Error should occur when trying to access the key file
|
|
if err == nil {
|
|
t.Fatal("Expected error when provisioner key file is not accessible")
|
|
}
|
|
}
|
|
|
|
func TestIssueCertificate_GetOrderStatus(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: "https://ca.example.com",
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
// GetOrderStatus should return immediately with "completed" status
|
|
status, err := connector.GetOrderStatus(ctx, "some-order-id")
|
|
if err != nil {
|
|
t.Fatalf("GetOrderStatus failed: %v", err)
|
|
}
|
|
|
|
if status.Status != "completed" {
|
|
t.Errorf("Expected status 'completed', got '%s'", status.Status)
|
|
}
|
|
|
|
if status.OrderID != "some-order-id" {
|
|
t.Errorf("Expected OrderID 'some-order-id', got '%s'", status.OrderID)
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_MarshalRequestError(t *testing.T) {
|
|
// Most marshal failures are hard to trigger, but we can test the happy path
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
w.WriteHeader(http.StatusOK)
|
|
case "/revoke":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
}
|
|
connector := stepca.New(config, logger)
|
|
|
|
reason := "keyCompromise"
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "12345678901234567890",
|
|
Reason: &reason,
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err != nil {
|
|
t.Fatalf("RevokeCertificate failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIntegration_FullLifecycle(t *testing.T) {
|
|
// Integration test covering full certificate lifecycle
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
testCertPEM, _ := generateTestCert(t)
|
|
callCount := struct {
|
|
health int
|
|
sign int
|
|
revoke int
|
|
}{}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/health":
|
|
callCount.health++
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
case "/sign":
|
|
callCount.sign++
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
resp := fmt.Sprintf(`{"crt": %q, "ca": %q}`, testCertPEM, testCertPEM)
|
|
w.Write([]byte(resp))
|
|
case "/revoke":
|
|
callCount.revoke++
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &stepca.Config{
|
|
CAURL: srv.URL,
|
|
ProvisionerName: "test-provisioner",
|
|
ValidityDays: 90,
|
|
}
|
|
|
|
// Test ValidateConfig
|
|
connector := stepca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
if err := connector.ValidateConfig(ctx, rawConfig); err != nil {
|
|
t.Fatalf("ValidateConfig failed: %v", err)
|
|
}
|
|
|
|
if callCount.health != 1 {
|
|
t.Errorf("Expected 1 health check, got %d", callCount.health)
|
|
}
|
|
|
|
// Create a new connector with validated config
|
|
connector = stepca.New(config, logger)
|
|
|
|
// Test IssueCertificate
|
|
_, csrPEM, _ := generateStepCATestCSR("app.internal.corp")
|
|
issueReq := issuer.IssuanceRequest{
|
|
CommonName: "app.internal.corp",
|
|
SANs: []string{"app.internal.corp", "app.example.com"},
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
issueResult, err := connector.IssueCertificate(ctx, issueReq)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate failed: %v", err)
|
|
}
|
|
|
|
if callCount.sign != 1 {
|
|
t.Errorf("Expected 1 sign call, got %d", callCount.sign)
|
|
}
|
|
|
|
if issueResult.Serial == "" {
|
|
t.Error("Expected non-empty serial")
|
|
}
|
|
|
|
// Test RenewCertificate
|
|
renewReq := issuer.RenewalRequest{
|
|
CommonName: "app.internal.corp",
|
|
SANs: []string{"app.internal.corp", "app.example.com"},
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
renewResult, err := connector.RenewCertificate(ctx, renewReq)
|
|
if err != nil {
|
|
t.Fatalf("RenewCertificate failed: %v", err)
|
|
}
|
|
|
|
if callCount.sign != 2 {
|
|
t.Errorf("Expected 2 sign calls after renewal, got %d", callCount.sign)
|
|
}
|
|
|
|
if renewResult.Serial == "" {
|
|
t.Error("Expected non-empty serial from renewal")
|
|
}
|
|
|
|
// Test RevokeCertificate
|
|
reason := "cessationOfOperation"
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: issueResult.Serial,
|
|
Reason: &reason,
|
|
}
|
|
|
|
if err := connector.RevokeCertificate(ctx, revokeReq); err != nil {
|
|
t.Fatalf("RevokeCertificate failed: %v", err)
|
|
}
|
|
|
|
if callCount.revoke != 1 {
|
|
t.Errorf("Expected 1 revoke call, got %d", callCount.revoke)
|
|
}
|
|
|
|
// Test GetOrderStatus
|
|
status, err := connector.GetOrderStatus(ctx, issueResult.OrderID)
|
|
if err != nil {
|
|
t.Fatalf("GetOrderStatus failed: %v", err)
|
|
}
|
|
|
|
if status.Status != "completed" {
|
|
t.Errorf("Expected status 'completed', got '%s'", status.Status)
|
|
}
|
|
}
|