package awsacm_test // Rank 5 of the 2026-05-03 Infisical deep-research deliverable // (the project's deep-research deliverable, Part 5). Per-error-class // failure tests for the AWS ACM target connector — mirrors the // awsacmpca_failure_test.go shape (commit 60dce0b) on the issuer side. // // Each test injects one specific AWS SDK v2 typed error via the // mockACMClient seam, calls DeployCertificate, and asserts: // // 1. error non-nil, // 2. errors.As against the SDK's typed error value succeeds (so the // wrap chain via fmt.Errorf("...%w", ...) is intact and upstream // retry / classification logic can introspect the typed value), // 3. operator-actionable substring is present in the surfaced // message, // 4. the failure category is correct (e.g. throttling = retryable; // validation = terminal). import ( "context" "errors" "strings" "testing" "github.com/aws/aws-sdk-go-v2/aws" acmtypes "github.com/aws/aws-sdk-go-v2/service/acm/types" smithy "github.com/aws/smithy-go" "github.com/certctl-io/certctl/internal/connector/target" "github.com/certctl-io/certctl/internal/connector/target/awsacm" ) // TestAWSACM_Issue_AccessDenied_OperatorActionableError pins the // behaviour when the IAM principal lacks acm:ImportCertificate. AWS // surfaces this as a smithy APIError with Code="AccessDeniedException" // (the ACM SDK does not generate a typed *types.AccessDeniedException // in v1.38.x — read it locally to confirm). func TestAWSACM_Issue_AccessDenied_OperatorActionableError(t *testing.T) { ctx := context.Background() certPEM, keyPEM, _ := generateTestCert(t, "denied.example.com") sdkErr := &smithy.GenericAPIError{ Code: "AccessDeniedException", Message: "User: arn:aws:iam::123456789012:user/ci is not authorized to perform: acm:ImportCertificate", Fault: smithy.FaultClient, } mock := &mockACMClient{importErr: sdkErr} c := awsacm.NewWithClient(&awsacm.Config{Region: "us-east-1"}, mock, quietTestLogger()) _, err := c.DeployCertificate(ctx, target.DeploymentRequest{ CertPEM: certPEM, KeyPEM: keyPEM, Metadata: map[string]string{"certificate_id": "mc-denied"}, }) if err == nil { t.Fatal("expected access-denied error, got nil") } var sdk *smithy.GenericAPIError if !errors.As(err, &sdk) { t.Fatalf("wrap chain broke — errors.As against *smithy.GenericAPIError failed; err=%v", err) } if sdk.ErrorCode() != "AccessDeniedException" { t.Errorf("expected ErrorCode=AccessDeniedException, got %q", sdk.ErrorCode()) } msg := err.Error() if !strings.Contains(msg, "AccessDenied") { t.Errorf("operator-actionable substring missing — message must mention AccessDenied; got: %s", msg) } if !strings.Contains(msg, "ImportCertificate failed") { t.Errorf("connector wrap missing — expected 'ImportCertificate failed: ...' framing; got: %s", msg) } } // TestAWSACM_Issue_ResourceNotFound_NamesTheMissingARN pins behaviour // when the configured CertificateArn doesn't exist (deleted out-of- // band, typo'd config, wrong region). The SDK's // *types.ResourceNotFoundException carries the ARN in its message; the // connector must preserve the ARN through the wrap chain so the // operator can identify which resource was missing. func TestAWSACM_Issue_ResourceNotFound_NamesTheMissingARN(t *testing.T) { ctx := context.Background() certPEM, keyPEM, _ := generateTestCert(t, "missing.example.com") missingArn := "arn:aws:acm:us-east-1:123456789012:certificate/deadbeef-dead-beef-dead-beefdeadbeef" sdkErr := &acmtypes.ResourceNotFoundException{ Message: aws.String("Could not find certificate " + missingArn), } // We need the snapshot read to fail — the connector calls // GetCertificate first to capture the snapshot. ResourceNotFound at // snapshot time means "no cert at this ARN," so we surface that // error and bail before the import. mock := &mockACMClient{getErr: sdkErr} cfg := &awsacm.Config{Region: "us-east-1", CertificateArn: missingArn} c := awsacm.NewWithClient(cfg, mock, quietTestLogger()) _, err := c.DeployCertificate(ctx, target.DeploymentRequest{ CertPEM: certPEM, KeyPEM: keyPEM, }) if err == nil { t.Fatal("expected resource-not-found error") } var sdk *acmtypes.ResourceNotFoundException if !errors.As(err, &sdk) { t.Fatalf("wrap chain broke — errors.As against *types.ResourceNotFoundException failed; err=%v", err) } msg := err.Error() if !strings.Contains(msg, missingArn) { t.Errorf("operator-actionable substring missing — message must name the missing ARN %q; got: %s", missingArn, msg) } if !strings.Contains(msg, "snapshot") { t.Errorf("expected 'snapshot' framing on pre-deploy snapshot failure; got: %s", msg) } } // TestAWSACM_Issue_Throttling_RetryableSurfacePreserved pins the // behaviour when ACM throttles a burst of imports (renewal storm, // bulk migration). Real traffic surfaces ThrottlingException via // *smithy.GenericAPIError; the connector must preserve the typed // value + Fault classification so any upstream retry layer can engage. // Per the spec's "no new retry logic" scope, the connector itself // does not retry; it surfaces the typed error. func TestAWSACM_Issue_Throttling_RetryableSurfacePreserved(t *testing.T) { ctx := context.Background() certPEM, keyPEM, _ := generateTestCert(t, "throttle.example.com") sdkErr := &smithy.GenericAPIError{ Code: "ThrottlingException", Message: "Rate exceeded", Fault: smithy.FaultServer, } mock := &mockACMClient{importErr: sdkErr} c := awsacm.NewWithClient(&awsacm.Config{Region: "us-east-1"}, mock, quietTestLogger()) _, err := c.DeployCertificate(ctx, target.DeploymentRequest{ CertPEM: certPEM, KeyPEM: keyPEM, Metadata: map[string]string{"certificate_id": "mc-throttle"}, }) if err == nil { t.Fatal("expected throttle error") } var sdk *smithy.GenericAPIError if !errors.As(err, &sdk) { t.Fatalf("wrap chain broke; err=%v", err) } if sdk.ErrorCode() != "ThrottlingException" { t.Errorf("expected ErrorCode=ThrottlingException, got %q", sdk.ErrorCode()) } if sdk.ErrorFault() != smithy.FaultServer { t.Errorf("expected FaultServer (retryable class) preserved; got %v", sdk.ErrorFault()) } if !strings.Contains(err.Error(), "Throttling") { t.Errorf("operator-actionable substring missing — message must mention Throttling; got: %s", err.Error()) } } // TestAWSACM_Issue_InvalidArgs_TerminalNotRetryable pins behaviour // when ACM rejects the cert+key as malformed (mismatched key/cert, // unsupported algorithm). InvalidArgsException is a terminal class — // operators must fix the inputs, not retry. func TestAWSACM_Issue_InvalidArgs_TerminalNotRetryable(t *testing.T) { ctx := context.Background() certPEM, keyPEM, _ := generateTestCert(t, "invalid.example.com") sdkErr := &acmtypes.InvalidArgsException{ Message: aws.String("The certificate body is invalid: chain not bound to leaf"), } mock := &mockACMClient{importErr: sdkErr} c := awsacm.NewWithClient(&awsacm.Config{Region: "us-east-1"}, mock, quietTestLogger()) _, err := c.DeployCertificate(ctx, target.DeploymentRequest{ CertPEM: certPEM, KeyPEM: keyPEM, Metadata: map[string]string{"certificate_id": "mc-invalid"}, }) if err == nil { t.Fatal("expected invalid-args error") } var sdk *acmtypes.InvalidArgsException if !errors.As(err, &sdk) { t.Fatalf("wrap chain broke; err=%v", err) } if !strings.Contains(err.Error(), "chain not bound") { t.Errorf("operator-actionable substring missing — message must name the validation issue; got: %s", err.Error()) } if !strings.Contains(err.Error(), "InvalidArgs") { t.Errorf("expected InvalidArgs in surfaced message; got: %s", err.Error()) } } // TestAWSACM_Issue_RequestInProgress_TerminalForCurrentAttempt pins // behaviour when ACM reports an in-flight request for the same // idempotency key. RequestInProgressException IS a generated typed // value; the connector must surface it cleanly so upstream logic can // decide whether to wait + retry or fail-fast. func TestAWSACM_Issue_RequestInProgress_TerminalForCurrentAttempt(t *testing.T) { ctx := context.Background() certPEM, keyPEM, _ := generateTestCert(t, "inprogress.example.com") sdkErr := &acmtypes.RequestInProgressException{ Message: aws.String("The certificate request is already being processed; resubmit after completion"), } mock := &mockACMClient{importErr: sdkErr} c := awsacm.NewWithClient(&awsacm.Config{Region: "us-east-1"}, mock, quietTestLogger()) _, err := c.DeployCertificate(ctx, target.DeploymentRequest{ CertPEM: certPEM, KeyPEM: keyPEM, Metadata: map[string]string{"certificate_id": "mc-inprogress"}, }) if err == nil { t.Fatal("expected request-in-progress error") } var sdk *acmtypes.RequestInProgressException if !errors.As(err, &sdk) { t.Fatalf("wrap chain broke; err=%v", err) } if !strings.Contains(err.Error(), "RequestInProgress") { t.Errorf("expected RequestInProgress in surfaced message; got: %s", err.Error()) } if !strings.Contains(err.Error(), "already being processed") { t.Errorf("operator-actionable substring missing — message must explain the conflict; got: %s", err.Error()) } }