package awsacmpca_test import ( "context" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/json" "encoding/pem" "errors" "fmt" "log/slog" "math/big" "os" "strings" "testing" "time" "github.com/certctl-io/certctl/internal/connector/issuer" "github.com/certctl-io/certctl/internal/connector/issuer/awsacmpca" ) // mockACMPCAClient implements the ACMPCAClient interface for testing. type mockACMPCAClient struct { issueCertificateErr error getCertificateErr error revokeCertificateErr error getCACertificateErr error issuedCertPEM string issuedChainPEM string caCertPEM string caCertChainPEM string lastIssueCertificateInput *awsacmpca.IssueCertificateInput lastRevokeCertificateInput *awsacmpca.RevokeCertificateInput } func (m *mockACMPCAClient) IssueCertificate(ctx context.Context, input *awsacmpca.IssueCertificateInput) (*awsacmpca.IssueCertificateOutput, error) { m.lastIssueCertificateInput = input if m.issueCertificateErr != nil { return nil, m.issueCertificateErr } return &awsacmpca.IssueCertificateOutput{ CertificateArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678/certificate/abcdef123456", }, nil } func (m *mockACMPCAClient) GetCertificate(ctx context.Context, input *awsacmpca.GetCertificateInput) (*awsacmpca.GetCertificateOutput, error) { if m.getCertificateErr != nil { return nil, m.getCertificateErr } return &awsacmpca.GetCertificateOutput{ Certificate: m.issuedCertPEM, CertificateChain: m.issuedChainPEM, }, nil } func (m *mockACMPCAClient) RevokeCertificate(ctx context.Context, input *awsacmpca.RevokeCertificateInput) error { m.lastRevokeCertificateInput = input return m.revokeCertificateErr } func (m *mockACMPCAClient) GetCACertificate(ctx context.Context, input *awsacmpca.GetCACertificateInput) (*awsacmpca.GetCACertificateOutput, error) { if m.getCACertificateErr != nil { return nil, m.getCACertificateErr } return &awsacmpca.GetCACertificateOutput{ Certificate: m.caCertPEM, CertificateChain: m.caCertChainPEM, }, nil } // mustNew is a test helper that calls awsacmpca.New and fails the test if // New returns an error. Use this for the ValidateConfig-only test sites // where config is nil; New(ctx, nil, ...) skips SDK loading and never // errors, so this helper is just to keep the call sites terse. The ctx // parameter exists for contextcheck-lint cleanliness — when callers have // ctx in scope, they should pass it through to New, which threads it // into the SDK config load. func mustNew(t *testing.T, ctx context.Context, config *awsacmpca.Config, logger *slog.Logger) *awsacmpca.Connector { t.Helper() c, err := awsacmpca.New(ctx, config, logger) if err != nil { t.Fatalf("awsacmpca.New: %v", err) } return c } // Helper function to generate a test certificate and CSR. func generateTestCertAndCSR(t *testing.T) (certPEM string, csrPEM string) { // Generate private key privKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("failed to generate private key: %v", err) } // Create certificate template serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { t.Fatalf("failed to generate serial number: %v", err) } template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ CommonName: "example.com", }, NotBefore: time.Now(), NotAfter: time.Now().AddDate(1, 0, 0), BasicConstraintsValid: true, IsCA: false, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, DNSNames: []string{"example.com", "www.example.com"}, } // Create self-signed certificate for testing certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) if err != nil { t.Fatalf("failed to create certificate: %v", err) } certPEM = string(pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: certDER, })) // Create CSR csrTemplate := x509.CertificateRequest{ Subject: pkix.Name{ CommonName: "example.com", }, DNSNames: []string{"example.com", "www.example.com"}, } csrDER, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, privKey) if err != nil { t.Fatalf("failed to create CSR: %v", err) } csrPEM = string(pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE REQUEST", Bytes: csrDER, })) return certPEM, csrPEM } func TestAWSACMPCAConnector(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) { config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", SigningAlgorithm: "SHA256WITHRSA", ValidityDays: 365, } connector := mustNew(t, ctx, nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err != nil { t.Fatalf("ValidateConfig failed: %v", err) } }) t.Run("ValidateConfig_AllOptionalFields", func(t *testing.T) { config := awsacmpca.Config{ Region: "eu-west-1", CAArn: "arn:aws:acm-pca:eu-west-1:123456789012:certificate-authority/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", SigningAlgorithm: "SHA512WITHECDSA", ValidityDays: 730, TemplateArn: "arn:aws:acm-pca:eu-west-1:123456789012:template/WebServer", } connector := mustNew(t, ctx, nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err != nil { t.Fatalf("ValidateConfig failed: %v", err) } }) t.Run("ValidateConfig_InvalidJSON", func(t *testing.T) { connector := mustNew(t, ctx, nil, logger) err := connector.ValidateConfig(ctx, []byte(`{invalid json}`)) if err == nil { t.Fatal("Expected error for invalid JSON") } if !strings.Contains(err.Error(), "invalid AWS ACM PCA config") { t.Errorf("Expected config error, got: %v", err) } }) t.Run("ValidateConfig_MissingRegion", func(t *testing.T) { config := awsacmpca.Config{ CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } connector := mustNew(t, ctx, nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for missing region") } if !strings.Contains(err.Error(), "region is required") { t.Errorf("Expected region required error, got: %v", err) } }) t.Run("ValidateConfig_MissingCAArn", func(t *testing.T) { config := awsacmpca.Config{ Region: "us-east-1", } connector := mustNew(t, ctx, nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for missing CA ARN") } if !strings.Contains(err.Error(), "CA ARN is required") { t.Errorf("Expected CA ARN required error, got: %v", err) } }) t.Run("ValidateConfig_InvalidCAArn", func(t *testing.T) { config := awsacmpca.Config{ Region: "us-east-1", CAArn: "not-an-arn", } connector := mustNew(t, ctx, nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for invalid CA ARN") } if !strings.Contains(err.Error(), "invalid CA ARN format") { t.Errorf("Expected invalid ARN error, got: %v", err) } }) t.Run("ValidateConfig_InvalidSigningAlgorithm", func(t *testing.T) { config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", SigningAlgorithm: "INVALID_ALGO", } connector := mustNew(t, ctx, nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for invalid signing algorithm") } if !strings.Contains(err.Error(), "invalid signing algorithm") { t.Errorf("Expected invalid algorithm error, got: %v", err) } }) t.Run("ValidateConfig_InvalidValidityDays", func(t *testing.T) { config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", ValidityDays: -1, } connector := mustNew(t, ctx, nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for negative validity days") } if !strings.Contains(err.Error(), "validity days must be non-negative") { t.Errorf("Expected validity days error, got: %v", err) } }) t.Run("IssueCertificate_Success", func(t *testing.T) { certPEM, csrPEM := generateTestCertAndCSR(t) mockClient := &mockACMPCAClient{ issuedCertPEM: certPEM, issuedChainPEM: certPEM, // Use same cert as chain for test } config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", SigningAlgorithm: "SHA256WITHRSA", ValidityDays: 365, } connector := awsacmpca.NewWithClient(&config, mockClient, logger) request := issuer.IssuanceRequest{ CommonName: "example.com", SANs: []string{"www.example.com"}, CSRPEM: csrPEM, } result, err := connector.IssueCertificate(ctx, request) if err != nil { t.Fatalf("IssueCertificate failed: %v", err) } if result.CertPEM == "" { t.Fatal("Expected certificate PEM in result") } if result.Serial == "" { t.Fatal("Expected serial number in result") } if result.OrderID == "" { t.Fatal("Expected OrderID (certificate ARN) in result") } }) t.Run("IssueCertificate_EmptyCSR", func(t *testing.T) { mockClient := &mockACMPCAClient{} config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } connector := awsacmpca.NewWithClient(&config, mockClient, logger) request := issuer.IssuanceRequest{ CommonName: "example.com", CSRPEM: "", // Empty CSR } _, err := connector.IssueCertificate(ctx, request) if err == nil { t.Fatal("Expected error for empty CSR") } if !strings.Contains(err.Error(), "failed to decode CSR PEM") { t.Errorf("Expected CSR decode error, got: %v", err) } }) t.Run("IssueCertificate_IssueError", func(t *testing.T) { certPEM, csrPEM := generateTestCertAndCSR(t) mockClient := &mockACMPCAClient{ issueCertificateErr: fmt.Errorf("AWS service error"), issuedCertPEM: certPEM, } config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } connector := awsacmpca.NewWithClient(&config, mockClient, logger) request := issuer.IssuanceRequest{ CommonName: "example.com", CSRPEM: csrPEM, } _, err := connector.IssueCertificate(ctx, request) if err == nil { t.Fatal("Expected error from IssueCertificate") } if !strings.Contains(err.Error(), "IssueCertificate failed") { t.Errorf("Expected issue error, got: %v", err) } }) t.Run("IssueCertificate_GetCertificateError", func(t *testing.T) { _, csrPEM := generateTestCertAndCSR(t) mockClient := &mockACMPCAClient{ getCertificateErr: fmt.Errorf("AWS service error"), } config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } connector := awsacmpca.NewWithClient(&config, mockClient, logger) request := issuer.IssuanceRequest{ CommonName: "example.com", CSRPEM: csrPEM, } _, err := connector.IssueCertificate(ctx, request) if err == nil { t.Fatal("Expected error from GetCertificate") } if !strings.Contains(err.Error(), "GetCertificate failed") { t.Errorf("Expected get cert error, got: %v", err) } }) t.Run("RenewCertificate_Success", func(t *testing.T) { certPEM, csrPEM := generateTestCertAndCSR(t) mockClient := &mockACMPCAClient{ issuedCertPEM: certPEM, issuedChainPEM: certPEM, } config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } connector := awsacmpca.NewWithClient(&config, mockClient, logger) request := issuer.RenewalRequest{ CommonName: "example.com", SANs: []string{"www.example.com"}, CSRPEM: csrPEM, } result, err := connector.RenewCertificate(ctx, request) if err != nil { t.Fatalf("RenewCertificate failed: %v", err) } if result.CertPEM == "" { t.Fatal("Expected certificate PEM in result") } }) t.Run("RevokeCertificate_Success", func(t *testing.T) { mockClient := &mockACMPCAClient{} config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } connector := awsacmpca.NewWithClient(&config, mockClient, logger) reason := "keyCompromise" request := issuer.RevocationRequest{ Serial: "aabbccdd123456", Reason: &reason, } err := connector.RevokeCertificate(ctx, request) if err != nil { t.Fatalf("RevokeCertificate failed: %v", err) } if mockClient.lastRevokeCertificateInput.RevocationReason != "KEY_COMPROMISE" { t.Errorf("Expected KEY_COMPROMISE reason, got: %s", mockClient.lastRevokeCertificateInput.RevocationReason) } }) t.Run("RevokeCertificate_WithDefaultReason", func(t *testing.T) { mockClient := &mockACMPCAClient{} config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } connector := awsacmpca.NewWithClient(&config, mockClient, logger) request := issuer.RevocationRequest{ Serial: "aabbccdd123456", Reason: nil, } err := connector.RevokeCertificate(ctx, request) if err != nil { t.Fatalf("RevokeCertificate failed: %v", err) } if mockClient.lastRevokeCertificateInput.RevocationReason != "UNSPECIFIED" { t.Errorf("Expected UNSPECIFIED reason, got: %s", mockClient.lastRevokeCertificateInput.RevocationReason) } }) t.Run("RevokeCertificate_Error", func(t *testing.T) { mockClient := &mockACMPCAClient{ revokeCertificateErr: fmt.Errorf("AWS service error"), } config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } connector := awsacmpca.NewWithClient(&config, mockClient, logger) request := issuer.RevocationRequest{ Serial: "aabbccdd123456", } err := connector.RevokeCertificate(ctx, request) if err == nil { t.Fatal("Expected error from RevokeCertificate") } }) t.Run("GetOrderStatus_ReturnsCompleted", func(t *testing.T) { mockClient := &mockACMPCAClient{} config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } connector := awsacmpca.NewWithClient(&config, mockClient, logger) status, err := connector.GetOrderStatus(ctx, "test-order-id") if err != nil { t.Fatalf("GetOrderStatus failed: %v", err) } if status.Status != "completed" { t.Errorf("Expected completed status, got: %s", status.Status) } }) t.Run("GetCACertPEM_Success", func(t *testing.T) { certPEM, _ := generateTestCertAndCSR(t) mockClient := &mockACMPCAClient{ caCertPEM: certPEM, } config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } connector := awsacmpca.NewWithClient(&config, mockClient, logger) caPEM, err := connector.GetCACertPEM(ctx) if err != nil { t.Fatalf("GetCACertPEM failed: %v", err) } if caPEM == "" { t.Fatal("Expected CA certificate PEM") } if !strings.Contains(caPEM, "CERTIFICATE") { t.Errorf("Expected PEM format, got: %s", caPEM) } }) t.Run("GetCACertPEM_WithChain", func(t *testing.T) { certPEM, _ := generateTestCertAndCSR(t) mockClient := &mockACMPCAClient{ caCertPEM: certPEM, caCertChainPEM: certPEM, } config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } connector := awsacmpca.NewWithClient(&config, mockClient, logger) caPEM, err := connector.GetCACertPEM(ctx) if err != nil { t.Fatalf("GetCACertPEM failed: %v", err) } // Should contain both certificate and chain separated by newline if !strings.Contains(caPEM, "\n") { t.Fatal("Expected certificate and chain combined") } }) t.Run("GetCACertPEM_Error", func(t *testing.T) { mockClient := &mockACMPCAClient{ getCACertificateErr: fmt.Errorf("AWS service error"), } config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } connector := awsacmpca.NewWithClient(&config, mockClient, logger) _, err := connector.GetCACertPEM(ctx) if err == nil { t.Fatal("Expected error from GetCACertPEM") } }) t.Run("GetRenewalInfo_ReturnsNil", func(t *testing.T) { mockClient := &mockACMPCAClient{} config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } connector := awsacmpca.NewWithClient(&config, mockClient, logger) result, err := connector.GetRenewalInfo(ctx, "cert-pem") if err != nil { t.Fatalf("GetRenewalInfo failed: %v", err) } if result != nil { t.Fatal("Expected nil result from GetRenewalInfo") } }) t.Run("ValidateConfig_AppliesDefaults", func(t *testing.T) { config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", // SigningAlgorithm and ValidityDays not set } connector := mustNew(t, ctx, nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err != nil { t.Fatalf("ValidateConfig failed: %v", err) } // Verify defaults were applied by checking the connector's config // Since config is private, we'll test via IssueCertificate to ensure algorithm is set }) t.Run("RevocationReason_Mapping", func(t *testing.T) { testCases := []struct { input string expected string }{ {"keyCompromise", "KEY_COMPROMISE"}, {"caCompromise", "CERTIFICATE_AUTHORITY_COMPROMISE"}, {"affiliationChanged", "AFFILIATION_CHANGED"}, {"superseded", "SUPERSEDED"}, {"cessationOfOperation", "CESSATION_OF_OPERATION"}, {"privilegeWithdrawn", "PRIVILEGE_WITHDRAWN"}, } for _, tc := range testCases { mockClient := &mockACMPCAClient{} config := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } connector := awsacmpca.NewWithClient(&config, mockClient, logger) reason := tc.input request := issuer.RevocationRequest{ Serial: "test-serial", Reason: &reason, } _ = connector.RevokeCertificate(ctx, request) if mockClient.lastRevokeCertificateInput.RevocationReason != tc.expected { t.Errorf("For reason %q, expected %q, got %q", tc.input, tc.expected, mockClient.lastRevokeCertificateInput.RevocationReason) } } }) } // TestNew_ProductionPath exercises the production New() path (NOT // NewWithClient). The audit's D11 blocker for AWSACMPCA was that tests // passed green via NewWithClient mock injection while the production // New() returned a stubClient that errored on every method. These tests // guard against that regression by verifying the production New() path // builds a real client end-to-end. // // The "client not initialized" sentinel string is the regression-marker: // the deleted stubClient returned an error containing that phrase from // every method. If anyone re-introduces a stub-style placeholder client // from New(), these tests fail because the production client is // non-stubby and doesn't return that sentinel. func TestNew_ProductionPath(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) ctx := context.Background() t.Run("ValidConfigBuildsRealClient", func(t *testing.T) { // New with a valid config calls awsconfig.LoadDefaultConfig + // acmpca.NewFromConfig. Should succeed even without AWS // credentials: LoadDefaultConfig sets up the credential chain // providers but doesn't actually fetch credentials until an // API call is made. cfg := &awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } c, err := awsacmpca.New(ctx, cfg, logger) if err != nil { t.Fatalf("New with valid config returned error: %v", err) } if c == nil { t.Fatal("New returned nil connector") } // Behavioral assertion: IssueCertificate with a bogus CSR fails // at PEM-decode (before any network call), and the error must // NOT be the deleted stubClient's "client not initialized" // sentinel. If anyone re-introduces a stub from production // New(), this assertion catches it. _, err = c.IssueCertificate(ctx, issuer.IssuanceRequest{ CommonName: "example.com", CSRPEM: "", // intentionally bogus }) if err == nil { t.Fatal("expected error from bogus CSR, got nil") } if strings.Contains(err.Error(), "not initialized") { t.Fatalf("got 'not initialized' error after New with valid config — production client was not wired: %v", err) } // Expected: PEM decode error. if !strings.Contains(err.Error(), "decode CSR PEM") { t.Errorf("expected CSR decode error, got: %v", err) } }) t.Run("NilConfigDefersClientInit", func(t *testing.T) { // New(nil, ...) is the test-only path that skips SDK loading; // the connector is constructed with no client and ValidateConfig // must be called before any operation. This documents the lazy // initialization contract. c, err := awsacmpca.New(ctx, nil, logger) if err != nil { t.Fatalf("New(nil, logger) returned error: %v", err) } if c == nil { t.Fatal("New(nil, ...) returned nil connector") } // Calling client-using methods before ValidateConfig should // fail-fast with the documented sentinel. _, err = c.IssueCertificate(ctx, issuer.IssuanceRequest{ CommonName: "example.com", CSRPEM: "", // bogus, but client-init check fires first }) if err == nil { t.Fatal("expected error from uninitialized client, got nil") } if !strings.Contains(err.Error(), "client not initialized") { t.Errorf("expected 'client not initialized' error, got: %v", err) } }) t.Run("ValidateConfigBuildsClientLazily", func(t *testing.T) { // New(nil, ...) leaves client nil; ValidateConfig with a valid // config should build it. After ValidateConfig succeeds, client- // using methods should work end-to-end (modulo network errors). c, err := awsacmpca.New(ctx, nil, logger) if err != nil { t.Fatalf("New(nil, logger): %v", err) } cfg := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } cfgJSON, _ := json.Marshal(cfg) if err := c.ValidateConfig(ctx, cfgJSON); err != nil { t.Fatalf("ValidateConfig: %v", err) } // IssueCertificate should now reach the PEM-decode step // (the client is wired). Bogus CSR triggers PEM error, // not the "client not initialized" sentinel. _, err = c.IssueCertificate(ctx, issuer.IssuanceRequest{ CommonName: "example.com", CSRPEM: "", }) if err == nil { t.Fatal("expected error from bogus CSR") } if strings.Contains(err.Error(), "not initialized") { t.Fatalf("ValidateConfig didn't wire client: %v", err) } }) t.Run("RevokeBeforeInitFailsFast", func(t *testing.T) { // The audit also flagged RevokeCertificate as part of the stub // blocker. Verify the nil-client guard fires for revoke too. c, err := awsacmpca.New(ctx, nil, logger) if err != nil { t.Fatalf("New(nil, logger): %v", err) } err = c.RevokeCertificate(ctx, issuer.RevocationRequest{ Serial: "aabbccdd", }) if err == nil { t.Fatal("expected error from uninitialized client") } if !strings.Contains(err.Error(), "client not initialized") { t.Errorf("expected 'client not initialized', got: %v", err) } }) t.Run("GetCAPEMBeforeInitFailsFast", func(t *testing.T) { c, err := awsacmpca.New(ctx, nil, logger) if err != nil { t.Fatalf("New(nil, logger): %v", err) } _, err = c.GetCACertPEM(ctx) if err == nil { t.Fatal("expected error from uninitialized client") } if !strings.Contains(err.Error(), "client not initialized") { t.Errorf("expected 'client not initialized', got: %v", err) } }) } // TestNew_ErrorPaths covers connector-level error paths via the mock // client. These complement the existing IssueCertificate_IssueError / // IssueCertificate_GetCertificateError tests by adding access-denied, // transient 5xx, and ctx-cancel coverage that the audit called out as // missing from D11. func TestNew_ErrorPaths(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) ctx := context.Background() cfg := awsacmpca.Config{ Region: "us-east-1", CAArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012", } t.Run("AccessDeniedSurfacedAsError", func(t *testing.T) { // Simulate the AWS access-denied case via the mock. The error // message format mirrors what aws-sdk-go-v2 surfaces for IAM // failures; the assertion is that the connector wraps the error // without swallowing it. mock := &mockACMPCAClient{ issueCertificateErr: fmt.Errorf("operation error ACM-PCA: IssueCertificate, https response error StatusCode: 403, RequestID: x, AccessDeniedException: User is not authorized to perform: acm-pca:IssueCertificate"), } c := awsacmpca.NewWithClient(&cfg, mock, logger) _, csrPEM := generateTestCertAndCSR(t) _, err := c.IssueCertificate(ctx, issuer.IssuanceRequest{ CommonName: "example.com", CSRPEM: csrPEM, }) if err == nil { t.Fatal("expected access-denied error") } if !strings.Contains(err.Error(), "AccessDenied") { t.Errorf("expected wrapped AccessDeniedException, got: %v", err) } if !strings.Contains(err.Error(), "IssueCertificate failed") { t.Errorf("expected connector wrapping ('IssueCertificate failed: ...'), got: %v", err) } }) t.Run("Transient5xxSurfacedAsError", func(t *testing.T) { // Simulate a transient 5xx from ACM PCA. The connector returns // the error to the caller; retry logic, if any, lives upstream // in the scheduler. mock := &mockACMPCAClient{ issueCertificateErr: fmt.Errorf("operation error ACM-PCA: IssueCertificate, https response error StatusCode: 503, ServiceUnavailable"), } c := awsacmpca.NewWithClient(&cfg, mock, logger) _, csrPEM := generateTestCertAndCSR(t) _, err := c.IssueCertificate(ctx, issuer.IssuanceRequest{ CommonName: "example.com", CSRPEM: csrPEM, }) if err == nil { t.Fatal("expected 5xx error") } if !strings.Contains(err.Error(), "503") && !strings.Contains(err.Error(), "ServiceUnavailable") { t.Errorf("expected wrapped 5xx, got: %v", err) } }) t.Run("CtxCancelPropagated", func(t *testing.T) { // Mock that respects ctx cancellation. Asserts the connector // honors caller-supplied deadlines. mock := &mockACMPCAClient{} c := awsacmpca.NewWithClient(&cfg, mock, logger) cancelCtx, cancel := context.WithCancel(ctx) cancel() // cancel immediately _, csrPEM := generateTestCertAndCSR(t) // The mock doesn't check ctx; we test by injecting a ctx-aware // error. Use a wrapped context.Canceled to simulate the SDK // returning a cancellation error. mock.issueCertificateErr = context.Canceled _, err := c.IssueCertificate(cancelCtx, issuer.IssuanceRequest{ CommonName: "example.com", CSRPEM: csrPEM, }) if err == nil { t.Fatal("expected ctx-cancel error") } if !errors.Is(err, context.Canceled) { t.Errorf("expected errors.Is(err, context.Canceled), got: %v", err) } }) }