package awssm import ( "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/base64" "encoding/pem" "fmt" "math/big" "testing" "time" "github.com/shankar0123/certctl/internal/config" "github.com/shankar0123/certctl/internal/domain" ) // mockSMClient is a mock implementation of SMClient for testing. type mockSMClient struct { secrets map[string]string // secret name -> secret value secretMetadata map[string]SecretMetadata // secret name -> metadata listError error getErrors map[string]error // secret name -> error } func newMockSMClient() *mockSMClient { return &mockSMClient{ secrets: make(map[string]string), secretMetadata: make(map[string]SecretMetadata), getErrors: make(map[string]error), } } func (m *mockSMClient) ListSecrets(ctx context.Context, filters string) ([]SecretMetadata, error) { if m.listError != nil { return nil, m.listError } var result []SecretMetadata for _, meta := range m.secretMetadata { result = append(result, meta) } return result, nil } func (m *mockSMClient) GetSecretValue(ctx context.Context, secretID string) (string, error) { if err, ok := m.getErrors[secretID]; ok { return "", err } return m.secrets[secretID], nil } // generateTestCert generates a test certificate with the given subject and returns it as PEM. func generateTestCert(commonName string, sans []string) (string, *x509.Certificate, error) { key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return "", nil, err } template := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: commonName}, NotBefore: time.Now().Add(-1 * time.Hour), NotAfter: time.Now().Add(24 * time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, DNSNames: sans, } certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) if err != nil { return "", nil, err } certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) cert, err := x509.ParseCertificate(certDER) if err != nil { return "", nil, err } return string(certPEM), cert, nil } func TestSource_ValidateConfig_Success(t *testing.T) { cfg := &config.AWSSecretsMgrDiscoveryConfig{ Enabled: true, Region: "us-east-1", } source := NewWithClient(cfg, newMockSMClient(), nil) err := source.ValidateConfig() if err != nil { t.Fatalf("expected no error, got %v", err) } } func TestSource_ValidateConfig_MissingRegion(t *testing.T) { cfg := &config.AWSSecretsMgrDiscoveryConfig{ Enabled: true, Region: "", } source := NewWithClient(cfg, newMockSMClient(), nil) err := source.ValidateConfig() if err == nil { t.Fatal("expected error for missing region") } if err.Error() != "aws secrets manager region is required" { t.Fatalf("unexpected error message: %v", err) } } func TestSource_Name(t *testing.T) { source := NewWithClient(&config.AWSSecretsMgrDiscoveryConfig{Region: "us-east-1"}, newMockSMClient(), nil) if source.Name() != "AWS Secrets Manager" { t.Errorf("expected 'AWS Secrets Manager', got %s", source.Name()) } } func TestSource_Type(t *testing.T) { source := NewWithClient(&config.AWSSecretsMgrDiscoveryConfig{Region: "us-east-1"}, newMockSMClient(), nil) if source.Type() != "aws-sm" { t.Errorf("expected 'aws-sm', got %s", source.Type()) } } func TestSource_Discover_Success(t *testing.T) { // Generate test certificates certPEM1, _, err := generateTestCert("test1.example.com", []string{"www.test1.example.com"}) if err != nil { t.Fatalf("failed to generate test cert 1: %v", err) } certPEM2, _, err := generateTestCert("test2.example.com", []string{"mail.test2.example.com", "smtp.test2.example.com"}) if err != nil { t.Fatalf("failed to generate test cert 2: %v", err) } // Set up mock client mockClient := newMockSMClient() mockClient.secrets["cert1"] = certPEM1 mockClient.secrets["cert2"] = certPEM2 mockClient.secretMetadata["cert1"] = SecretMetadata{ Name: "cert1", ARN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:cert1", Tags: map[string]string{"type": "certificate"}, } mockClient.secretMetadata["cert2"] = SecretMetadata{ Name: "cert2", ARN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:cert2", Tags: map[string]string{"type": "certificate"}, } cfg := &config.AWSSecretsMgrDiscoveryConfig{ Enabled: true, Region: "us-east-1", TagFilter: "type=certificate", } source := NewWithClient(cfg, mockClient, nil) report, err := source.Discover(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } if report.AgentID != "cloud-aws-sm" { t.Errorf("expected agent ID 'cloud-aws-sm', got %s", report.AgentID) } if len(report.Certificates) != 2 { t.Errorf("expected 2 certificates, got %d", len(report.Certificates)) } // Find the certificates by common name (order is not guaranteed) var cert1, cert2 *domain.DiscoveredCertEntry for i := range report.Certificates { if report.Certificates[i].CommonName == "test1.example.com" { cert1 = &report.Certificates[i] } else if report.Certificates[i].CommonName == "test2.example.com" { cert2 = &report.Certificates[i] } } if cert1 == nil { t.Fatalf("certificate with CN 'test1.example.com' not found") } if cert2 == nil { t.Fatalf("certificate with CN 'test2.example.com' not found") } // Check first certificate if len(cert1.SANs) != 1 || cert1.SANs[0] != "www.test1.example.com" { t.Errorf("unexpected SANs for cert1: %v", cert1.SANs) } // Check second certificate has 2 SANs if len(cert2.SANs) != 2 { t.Errorf("expected 2 SANs for cert2, got %d", len(cert2.SANs)) } // Check source path format for first cert if cert1.SourcePath != "aws-sm://us-east-1/cert1" { t.Errorf("unexpected source path for cert1: %s", cert1.SourcePath) } // Check that scan duration is reasonable if report.ScanDurationMs < 0 { t.Errorf("unexpected negative scan duration: %d", report.ScanDurationMs) } } func TestSource_Discover_EmptyResults(t *testing.T) { mockClient := newMockSMClient() cfg := &config.AWSSecretsMgrDiscoveryConfig{ Enabled: true, Region: "us-east-1", } source := NewWithClient(cfg, mockClient, nil) report, err := source.Discover(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } if report.AgentID != "cloud-aws-sm" { t.Errorf("expected agent ID 'cloud-aws-sm', got %s", report.AgentID) } if len(report.Certificates) != 0 { t.Errorf("expected 0 certificates, got %d", len(report.Certificates)) } if len(report.Errors) != 0 { t.Errorf("expected 0 errors, got %d", len(report.Errors)) } } func TestSource_Discover_ListError(t *testing.T) { mockClient := newMockSMClient() mockClient.listError = fmt.Errorf("ListSecrets failed") cfg := &config.AWSSecretsMgrDiscoveryConfig{ Enabled: true, Region: "us-east-1", } source := NewWithClient(cfg, mockClient, nil) report, err := source.Discover(context.Background()) if err != nil { t.Fatalf("Discover should not return error for list failure: %v", err) } // Should have recorded the error but still return a report if len(report.Errors) != 1 { t.Errorf("expected 1 error, got %d", len(report.Errors)) } } func TestSource_Discover_GetSecretError(t *testing.T) { // Generate test certificate certPEM, _, err := generateTestCert("good.example.com", nil) if err != nil { t.Fatalf("failed to generate test cert: %v", err) } mockClient := newMockSMClient() mockClient.secrets["good-secret"] = certPEM mockClient.secretMetadata["good-secret"] = SecretMetadata{ Name: "good-secret", Tags: map[string]string{"type": "certificate"}, } mockClient.secrets["bad-secret"] = "dummy" mockClient.secretMetadata["bad-secret"] = SecretMetadata{ Name: "bad-secret", Tags: map[string]string{"type": "certificate"}, } mockClient.getErrors["bad-secret"] = fmt.Errorf("GetSecretValue failed") cfg := &config.AWSSecretsMgrDiscoveryConfig{ Enabled: true, Region: "us-east-1", } source := NewWithClient(cfg, mockClient, nil) report, err := source.Discover(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } // Should have 1 good certificate and 1 error if len(report.Certificates) != 1 { t.Errorf("expected 1 certificate, got %d", len(report.Certificates)) } if len(report.Errors) != 1 { t.Errorf("expected 1 error, got %d", len(report.Errors)) } } func TestSource_Discover_DERCert(t *testing.T) { // Generate test certificate in DER format, then base64 encode it _, parsedCert, err := generateTestCert("der.example.com", nil) if err != nil { t.Fatalf("failed to generate test cert: %v", err) } derEncoded := base64.StdEncoding.EncodeToString(parsedCert.Raw) mockClient := newMockSMClient() mockClient.secrets["der-cert"] = derEncoded mockClient.secretMetadata["der-cert"] = SecretMetadata{ Name: "der-cert", Tags: map[string]string{"type": "certificate"}, } cfg := &config.AWSSecretsMgrDiscoveryConfig{ Enabled: true, Region: "us-east-1", } source := NewWithClient(cfg, mockClient, nil) report, err := source.Discover(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(report.Certificates) != 1 { t.Errorf("expected 1 certificate, got %d", len(report.Certificates)) } if report.Certificates[0].CommonName != "der.example.com" { t.Errorf("expected CN 'der.example.com', got %s", report.Certificates[0].CommonName) } } func TestSource_Discover_AgentIDAndSourcePath(t *testing.T) { // Generate test certificate certPEM, _, err := generateTestCert("source-path.example.com", nil) if err != nil { t.Fatalf("failed to generate test cert: %v", err) } mockClient := newMockSMClient() mockClient.secrets["my-secret"] = certPEM mockClient.secretMetadata["my-secret"] = SecretMetadata{ Name: "my-secret", Tags: map[string]string{"type": "certificate"}, } cfg := &config.AWSSecretsMgrDiscoveryConfig{ Enabled: true, Region: "eu-west-1", } source := NewWithClient(cfg, mockClient, nil) report, err := source.Discover(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) } if report.AgentID != "cloud-aws-sm" { t.Errorf("expected agent ID 'cloud-aws-sm', got %s", report.AgentID) } if report.Certificates[0].SourcePath != "aws-sm://eu-west-1/my-secret" { t.Errorf("expected source path 'aws-sm://eu-west-1/my-secret', got %s", report.Certificates[0].SourcePath) } }