Files
shankar0123 8b75e0311b chore: rename Go module path to github.com/certctl-io/certctl
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.
2026-05-04 00:30:29 +00:00

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)
}
}