mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
test: use 1024-bit keys and parallelise ACM handler tests
This commit is contained in:
@@ -31,10 +31,11 @@ const (
|
||||
|
||||
// genAuditShapedCert builds a certificate shaped like the one the audit
|
||||
// imported into real ACM (openssl req -x509 -newkey rsa:2048 -subj /CN=...):
|
||||
// RSA-2048, SHA-256 with RSA, CN only, no SAN, key-usage or EKU extension.
|
||||
// SHA-256 with RSA, CN only, no SAN, key-usage or EKU extension. The key is
|
||||
// 1024 bits rather than the audit's 2048 to keep generation fast.
|
||||
func genAuditShapedCert(t *testing.T) (certPEM, keyPEM []byte) {
|
||||
t.Helper()
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err)
|
||||
serial, ok := new(big.Int).SetString(awsAuditSerialHex, 16)
|
||||
require.True(t, ok)
|
||||
@@ -82,6 +83,7 @@ func wireJSON(t *testing.T, v any) map[string]any {
|
||||
// Expected values are copied from real ACM's DescribeCertificate response for
|
||||
// the audit's certificate. Export is absent: aws-sdk-go v1 cannot carry it.
|
||||
func TestDescribeCertificate_ImportedMatchesAWSAuditResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
certPEM, keyPEM := genAuditShapedCert(t)
|
||||
d := describeCert(t, svc, importCert(t, svc, certPEM, keyPEM))
|
||||
@@ -105,6 +107,7 @@ func TestDescribeCertificate_ImportedMatchesAWSAuditResponse(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListCertificates_ImportedSummaryMatchesAWSAuditResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
certPEM, keyPEM := genAuditShapedCert(t)
|
||||
importCert(t, svc, certPEM, keyPEM)
|
||||
@@ -125,6 +128,7 @@ func TestListCertificates_ImportedSummaryMatchesAWSAuditResponse(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImportCertificate_ReimportKeepsCreatedAtAndMovesImportedAt(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c1, k1 := genAuditShapedCert(t)
|
||||
certArn := importCert(t, svc, c1, k1)
|
||||
@@ -144,6 +148,7 @@ func TestImportCertificate_ReimportKeepsCreatedAtAndMovesImportedAt(t *testing.T
|
||||
// A record stored before these fields existed holds the old Serial and Issuer
|
||||
// spellings, no SANs and no CreatedAt; it must still describe as AWS does.
|
||||
func TestDescribeCertificate_LegacyImportedRecordDerivedFromPEM(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
certPEM, keyPEM := genAuditShapedCert(t)
|
||||
certArn := svc.mintCertificateArn(testAccountID)
|
||||
@@ -175,6 +180,7 @@ func TestDescribeCertificate_LegacyImportedRecordDerivedFromPEM(t *testing.T) {
|
||||
// a self-signed P-384 import carrying an O, SANs that omit the CN, and both
|
||||
// usage extensions.
|
||||
func TestDescribeCertificate_ImportedWithExtensionsMatchesAWS(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func TestGetCertificate_ReturnsBodyAndChain(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
leafPEM, keyPEM := genCert(t, "get.example.com")
|
||||
chainPEM, _ := genCert(t, "issuer.example.com")
|
||||
@@ -36,6 +37,7 @@ func TestGetCertificate_ReturnsBodyAndChain(t *testing.T) {
|
||||
// itself as the chain. The substitution is response-only: the stored record
|
||||
// keeps no chain, which is what ELBv2 builds its leaf+chain bundle from.
|
||||
func TestGetCertificate_AbsentChainReturnsCertificate(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
leafPEM, keyPEM := genCert(t, "nochain.example.com")
|
||||
|
||||
@@ -62,6 +64,7 @@ func TestGetCertificate_AbsentChainReturnsCertificate(t *testing.T) {
|
||||
// A re-import without a chain drops the chain the first import carried, so
|
||||
// GetCertificate falls back to the new certificate, not the stale chain.
|
||||
func TestGetCertificate_ReimportWithoutChainReturnsNewCertificate(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
leafPEM, keyPEM := genCert(t, "reimport.example.com")
|
||||
chainPEM, _ := genCert(t, "issuer.example.com")
|
||||
@@ -90,6 +93,7 @@ func TestGetCertificate_ReimportWithoutChainReturnsNewCertificate(t *testing.T)
|
||||
}
|
||||
|
||||
func TestGetCertificate_UnknownArnNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
|
||||
_, err := svc.GetCertificate(context.Background(), &acm.GetCertificateInput{
|
||||
@@ -103,6 +107,7 @@ func TestGetCertificate_UnknownArnNotFound(t *testing.T) {
|
||||
// body is the material itself, so a cross-account read must not confirm the
|
||||
// certificate even exists.
|
||||
func TestGetCertificate_CrossAccountNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
leafPEM, keyPEM := genCert(t, "other.example.com")
|
||||
|
||||
@@ -123,6 +128,7 @@ func TestGetCertificate_CrossAccountNotFound(t *testing.T) {
|
||||
// and distinct from not-found, so a client polling for issuance can tell them
|
||||
// apart.
|
||||
func TestGetCertificate_PendingIssuanceIsRequestInProgress(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
arn := "arn:aws:acm:ap-southeast-2:000000000001:certificate/pending-1"
|
||||
require.NoError(t, svc.store.PutCert(t.Context(), &CertRecord{
|
||||
@@ -140,6 +146,7 @@ func TestGetCertificate_PendingIssuanceIsRequestInProgress(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetCertificate_EmptyArnRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
|
||||
_, err := svc.GetCertificate(context.Background(), &acm.GetCertificateInput{}, testAccountID)
|
||||
@@ -170,6 +177,7 @@ func putUndecryptableCert(t *testing.T, store *Store, arn, domain string) {
|
||||
// than instrumentation: an undecryptable key is fatal to a decrypting read, so a
|
||||
// list that still returns the certificate cannot have decrypted it.
|
||||
func TestListPath_NeverTouchesKeyMaterial(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
arn := "arn:aws:acm:ap-southeast-2:000000000001:certificate/nokeyread-1"
|
||||
putUndecryptableCert(t, svc.store, arn, "nokeyread.example.com")
|
||||
@@ -202,6 +210,7 @@ func TestListPath_NeverTouchesKeyMaterial(t *testing.T) {
|
||||
// caller cannot mistake ciphertext for usable key material in a field that is
|
||||
// plaintext PEM everywhere else.
|
||||
func TestMetadataReads_ClearPrivateKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
arn := "arn:aws:acm:ap-southeast-2:000000000001:certificate/cleared-1"
|
||||
_, keyPEM := genCert(t, "cleared.example.com")
|
||||
@@ -230,6 +239,7 @@ func TestMetadataReads_ClearPrivateKey(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetCertMetadata_AbsentReturnsNil(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
|
||||
rec, err := store.GetCertMetadata(t.Context(), "arn:aws:acm:ap-southeast-2:000000000001:certificate/missing")
|
||||
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
|
||||
// genRSACert returns a self-signed RSA leaf with only a CN, the shape of the
|
||||
// certificate the AWS conformance audit imported.
|
||||
func genRSACert(t *testing.T, cn string, bits int) (certPEM, keyPEM []byte) {
|
||||
func genRSACert(t *testing.T, cn string) (certPEM, keyPEM []byte) {
|
||||
t.Helper()
|
||||
key, err := rsa.GenerateKey(rand.Reader, bits)
|
||||
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err)
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
@@ -57,11 +57,12 @@ func describeAndListKeyAlgorithm(t *testing.T, svc *ACMServiceImpl, certArn stri
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// AWS returns RSA-2048 with a hyphen from both Describe and List, although the
|
||||
// API enum (and the ListCertificates keyTypes filter) spells it RSA_2048.
|
||||
// AWS returns RSA-1024 with a hyphen from both Describe and List, although the
|
||||
// API enum (and the ListCertificates keyTypes filter) spells it RSA_1024.
|
||||
func TestImportCertificate_RSAKeyAlgorithmIsHyphenated(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
certPEM, keyPEM := genRSACert(t, "rsa.example.com", 2048)
|
||||
certPEM, keyPEM := genRSACert(t, "rsa.example.com")
|
||||
|
||||
out, err := svc.ImportCertificate(context.Background(), &acm.ImportCertificateInput{
|
||||
Certificate: certPEM,
|
||||
@@ -70,18 +71,19 @@ func TestImportCertificate_RSAKeyAlgorithmIsHyphenated(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
described, listed := describeAndListKeyAlgorithm(t, svc, aws.StringValue(out.CertificateArn))
|
||||
assert.Equal(t, "RSA-2048", described)
|
||||
assert.Equal(t, "RSA-2048", listed)
|
||||
assert.Equal(t, "RSA-1024", described)
|
||||
assert.Equal(t, "RSA-1024", listed)
|
||||
|
||||
// The read path normalises, so check the stored spelling separately.
|
||||
raw, _, err := svc.store.store.Get(t.Context(), certKey(aws.StringValue(out.CertificateArn)))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "RSA-2048", raw.KeyAlgorithm)
|
||||
assert.Equal(t, "RSA-1024", raw.KeyAlgorithm)
|
||||
}
|
||||
|
||||
// Records written before the spelling fix hold RSA_<bits> or EC_<Go curve>;
|
||||
// they must read back in the AWS spelling without a migration.
|
||||
func TestKeyAlgorithm_StoredUnderscoreSpellingReadsBackHyphenated(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
stored, want string
|
||||
}{
|
||||
@@ -93,6 +95,7 @@ func TestKeyAlgorithm_StoredUnderscoreSpellingReadsBackHyphenated(t *testing.T)
|
||||
svc := setupACMService(t)
|
||||
for i, tc := range cases {
|
||||
t.Run(tc.stored, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
certArn := "arn:aws:acm:ap-southeast-2:" + testAccountID + ":certificate/legacy-" + string(rune('a'+i))
|
||||
require.NoError(t, svc.store.PutCert(t.Context(), &CertRecord{
|
||||
CertificateArn: certArn,
|
||||
@@ -111,8 +114,9 @@ func TestKeyAlgorithm_StoredUnderscoreSpellingReadsBackHyphenated(t *testing.T)
|
||||
// AWS refuses a re-import whose key type differs from the current one, naming
|
||||
// both in the API enum spelling.
|
||||
func TestImportCertificate_ReimportWithDifferentKeyTypeRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c1, k1 := genRSACert(t, "k.example.com", 2048)
|
||||
c1, k1 := genRSACert(t, "k.example.com")
|
||||
out, err := svc.ImportCertificate(context.Background(), &acm.ImportCertificateInput{Certificate: c1, PrivateKey: k1}, testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -123,12 +127,13 @@ func TestImportCertificate_ReimportWithDifferentKeyTypeRejected(t *testing.T) {
|
||||
code, msg, ok := awserrors.ResolveErrorDetail(err)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, awserrors.ErrorValidationException, code)
|
||||
assert.Equal(t, "New certificate has a key of EC_prime256v1 which is different from RSA_2048 in the current certificate.", msg)
|
||||
assert.Equal(t, "New certificate has a key of EC_prime256v1 which is different from RSA_1024 in the current certificate.", msg)
|
||||
}
|
||||
|
||||
func TestImportCertificate_SerialZeroRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
require.NoError(t, err)
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(0),
|
||||
|
||||
@@ -17,13 +17,11 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestMain lowers the RSA key sizes for the whole privateca test file. 2048
|
||||
// (CA) and 1024 (leaf) bits are still valid RSA keys and generate several
|
||||
// times faster than the production defaults, and the tests here care about
|
||||
// certificate structure, not cryptographic strength — mirrors
|
||||
// admin.certKeyBits's test seam.
|
||||
// TestMain lowers the RSA key sizes for the whole package to 1024 bits, the
|
||||
// smallest crypto/rsa accepts; the tests care about certificate structure,
|
||||
// not cryptographic strength — mirrors admin.certKeyBits's test seam.
|
||||
func TestMain(m *testing.M) {
|
||||
caKeyBits = 2048
|
||||
caKeyBits = 1024
|
||||
leafKeyBits = 1024
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
@@ -35,6 +33,7 @@ func tenantCAPaths(t *testing.T) (certPath, keyPath string) {
|
||||
}
|
||||
|
||||
func TestLoadOrCreateTenantCA_CreatesConstrainedRoot(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
|
||||
ca, err := LoadOrCreateTenantCA(certPath, keyPath, []string{"home.example.com"})
|
||||
@@ -79,6 +78,7 @@ func TestLoadOrCreateTenantCA_CreatesConstrainedRoot(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadOrCreateTenantCA_RefusesEmptyPermittedDomains(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
|
||||
_, err := LoadOrCreateTenantCA(certPath, keyPath, nil)
|
||||
@@ -88,6 +88,7 @@ func TestLoadOrCreateTenantCA_RefusesEmptyPermittedDomains(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadOrCreateTenantCA_Idempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
|
||||
first, err := LoadOrCreateTenantCA(certPath, keyPath, []string{"home.example.com"})
|
||||
@@ -111,6 +112,7 @@ func TestLoadOrCreateTenantCA_Idempotent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadOrCreateTenantCA_DriftIsRejectedAndLeavesRootUntouched(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
|
||||
_, err := LoadOrCreateTenantCA(certPath, keyPath, []string{"home.example.com"})
|
||||
@@ -135,6 +137,7 @@ func TestLoadOrCreateTenantCA_DriftIsRejectedAndLeavesRootUntouched(t *testing.T
|
||||
}
|
||||
|
||||
func TestLoadOrCreateTenantCA_SubdomainRequestCoveredByExistingApex(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
|
||||
_, err := LoadOrCreateTenantCA(certPath, keyPath, []string{"example.com"})
|
||||
@@ -147,6 +150,7 @@ func TestLoadOrCreateTenantCA_SubdomainRequestCoveredByExistingApex(t *testing.T
|
||||
}
|
||||
|
||||
func TestLoadOrCreateTenantCA_IncompletePairIsRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
|
||||
_, err := LoadOrCreateTenantCA(certPath, keyPath, []string{"home.example.com"})
|
||||
@@ -161,6 +165,7 @@ func TestLoadOrCreateTenantCA_IncompletePairIsRejected(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIssueLeaf_ChainsToRootAndCoversWildcard(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
ca, err := LoadOrCreateTenantCA(certPath, keyPath, []string{"home.example.com"})
|
||||
require.NoError(t, err)
|
||||
@@ -193,6 +198,7 @@ func TestIssueLeaf_ChainsToRootAndCoversWildcard(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIssueLeaf_RejectsDomainOutsidePermittedDomains(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
ca, err := LoadOrCreateTenantCA(certPath, keyPath, []string{"home.example.com"})
|
||||
require.NoError(t, err)
|
||||
@@ -211,6 +217,7 @@ func TestIssueLeaf_RejectsDomainOutsidePermittedDomains(t *testing.T) {
|
||||
// calls out. If this leaf were to verify, the constraints baked into the
|
||||
// root would be decorative, not enforced.
|
||||
func TestLeafOutsidePermittedDomains_FailsVerify(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
ca, err := LoadOrCreateTenantCA(certPath, keyPath, []string{"home.example.com"})
|
||||
require.NoError(t, err)
|
||||
@@ -240,6 +247,7 @@ func TestLeafOutsidePermittedDomains_FailsVerify(t *testing.T) {
|
||||
// the CA key so the test does not depend on IssueLeaf ever choosing to
|
||||
// offer an IP SAN option.
|
||||
func TestLeafWithIPSAN_FailsVerify(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
ca, err := LoadOrCreateTenantCA(certPath, keyPath, []string{"home.example.com"})
|
||||
require.NoError(t, err)
|
||||
@@ -270,6 +278,7 @@ func TestLeafWithIPSAN_FailsVerify(t *testing.T) {
|
||||
// one), uses that intermediate to sign a leaf, and asserts the resulting
|
||||
// chain fails verification even though every individual signature is valid.
|
||||
func TestTenantRootCannotSignSubordinateCA(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
ca, err := LoadOrCreateTenantCA(certPath, keyPath, []string{"home.example.com"})
|
||||
require.NoError(t, err)
|
||||
@@ -320,6 +329,7 @@ func TestTenantRootCannotSignSubordinateCA(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthorized(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
ca, err := LoadOrCreateTenantCA(certPath, keyPath, []string{"home.example.com"})
|
||||
require.NoError(t, err)
|
||||
@@ -342,12 +352,14 @@ func TestAuthorized(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, tc.want, ca.Authorized(tc.domain))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTenantCA_MissingReturnsActionableError(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
|
||||
_, err := LoadTenantCA(certPath, keyPath)
|
||||
@@ -356,6 +368,7 @@ func TestLoadTenantCA_MissingReturnsActionableError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadTenantCA_IncompletePairReturnsActionableError(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
|
||||
_, err := LoadOrCreateTenantCA(certPath, keyPath, []string{"home.example.com"})
|
||||
@@ -369,6 +382,7 @@ func TestLoadTenantCA_IncompletePairReturnsActionableError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadTenantCA_RoundTripsCreatedCA(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
|
||||
created, err := LoadOrCreateTenantCA(certPath, keyPath, []string{"home.example.com", "lab.example.org"})
|
||||
@@ -391,6 +405,7 @@ func TestLoadTenantCA_RoundTripsCreatedCA(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPermittedDomains_ReturnsDefensiveCopy(t *testing.T) {
|
||||
t.Parallel()
|
||||
certPath, keyPath := tenantCAPaths(t)
|
||||
ca, err := LoadOrCreateTenantCA(certPath, keyPath, []string{"home.example.com"})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
// exactly on renewalDueAt rather than depending on it landing on a whole
|
||||
// unit of time.
|
||||
func TestRenewable_ProportionalWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
const arn = "arn:aws:acm:ap-southeast-2:1:certificate/window-test"
|
||||
jitter := renewalJitter(arn)
|
||||
now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
@@ -38,6 +39,7 @@ func TestRenewable_ProportionalWindow(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("not yet due", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
lifetime := 90 * 24 * time.Hour
|
||||
window := renewalWindow(lifetime)
|
||||
rec := newRec(lifetime, window+jitter-time.Minute)
|
||||
@@ -45,6 +47,7 @@ func TestRenewable_ProportionalWindow(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("exactly at threshold", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
lifetime := 90 * 24 * time.Hour
|
||||
window := renewalWindow(lifetime)
|
||||
rec := newRec(lifetime, window+jitter)
|
||||
@@ -52,6 +55,7 @@ func TestRenewable_ProportionalWindow(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("overdue", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
lifetime := 90 * 24 * time.Hour
|
||||
window := renewalWindow(lifetime)
|
||||
rec := newRec(lifetime, window+jitter+24*time.Hour)
|
||||
@@ -59,6 +63,7 @@ func TestRenewable_ProportionalWindow(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("very short lifetime hits the floor", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
lifetime := 10 * time.Minute
|
||||
window := renewalWindow(lifetime)
|
||||
require.Equal(t, minRenewalWindow, window,
|
||||
@@ -78,6 +83,7 @@ func TestRenewable_ProportionalWindow(t *testing.T) {
|
||||
// only a PRIVATE, ISSUED, ELIGIBLE certificate is ever renewable, regardless
|
||||
// of how overdue it is.
|
||||
func TestRenewable_GatesOnTypeStatusEligibility(t *testing.T) {
|
||||
t.Parallel()
|
||||
now := time.Now()
|
||||
overdue := func() *CertRecord {
|
||||
return &CertRecord{
|
||||
@@ -102,6 +108,7 @@ func TestRenewable_GatesOnTypeStatusEligibility(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
rec := overdue()
|
||||
tt.mutate(rec)
|
||||
assert.False(t, renewable(rec, now))
|
||||
@@ -112,6 +119,7 @@ func TestRenewable_GatesOnTypeStatusEligibility(t *testing.T) {
|
||||
// --- Deterministic per-ARN jitter -------------------------------------------
|
||||
|
||||
func TestRenewalJitter_DeterministicPerARN(t *testing.T) {
|
||||
t.Parallel()
|
||||
const arn = "arn:aws:acm:ap-southeast-2:1:certificate/jitter-test"
|
||||
first := renewalJitter(arn)
|
||||
second := renewalJitter(arn)
|
||||
@@ -121,6 +129,7 @@ func TestRenewalJitter_DeterministicPerARN(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRenewalJitter_SpreadsAcrossDistinctARNs(t *testing.T) {
|
||||
t.Parallel()
|
||||
offsets := make(map[time.Duration]bool)
|
||||
for i := range 8 {
|
||||
arn := fmt.Sprintf("arn:aws:acm:ap-southeast-2:1:certificate/cert-%d", i)
|
||||
@@ -145,6 +154,7 @@ func backdateToOverdue(t *testing.T, svc *ACMServiceImpl, certArn string) {
|
||||
// TestWorker_RenewOne_PreservesIdentityChangesKeyAndSerial is the core
|
||||
// renewal contract: same ARN, same domain and SANs, fresh key and serial.
|
||||
func TestWorker_RenewOne_PreservesIdentityChangesKeyAndSerial(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
svc.TenantCA = &fakeCertAuthority{permitted: map[string]bool{
|
||||
"renew.example.com": true,
|
||||
@@ -185,6 +195,7 @@ func TestWorker_RenewOne_PreservesIdentityChangesKeyAndSerial(t *testing.T) {
|
||||
// mandatory push described in the ACM design doc's ELBv2 propagation
|
||||
// section.
|
||||
func TestWorker_CertMaterialUpdated_InvokedExactlyOnceOnSuccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
svc.TenantCA = &fakeCertAuthority{permitted: map[string]bool{"fanout.example.com": true}}
|
||||
var calls int
|
||||
@@ -215,6 +226,7 @@ func TestWorker_CertMaterialUpdated_InvokedExactlyOnceOnSuccess(t *testing.T) {
|
||||
// scan over a mix of certificate shapes and asserts only the eligible,
|
||||
// overdue PRIVATE_CA certificate is renewed.
|
||||
func TestWorker_ScanOnce_SkipsIneligibleAndImported_RenewsEligible(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
svc.TenantCA = &fakeCertAuthority{permitted: map[string]bool{
|
||||
"eligible.example.com": true,
|
||||
@@ -267,6 +279,7 @@ func TestWorker_ScanOnce_SkipsIneligibleAndImported_RenewsEligible(t *testing.T)
|
||||
// RenewalSummary (the sole DescribeCertificate diagnostic surface) and must
|
||||
// not re-attempt on the very next scan.
|
||||
func TestWorker_ScanOnce_DomainOutsideConstraints_RecordsFailureAndBacksOff(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
ca := &fakeCertAuthority{permitted: map[string]bool{"narrowed.example.com": true}}
|
||||
svc.TenantCA = ca
|
||||
@@ -305,6 +318,7 @@ func TestWorker_ScanOnce_DomainOutsideConstraints_RecordsFailureAndBacksOff(t *t
|
||||
// lease actually excludes a second worker: node-a's live lease must stop
|
||||
// node-b's worker from reissuing the same certificate underneath it.
|
||||
func TestWorker_LeaseContention_OnlyOneNodeRenews(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
svc.TenantCA = &fakeCertAuthority{permitted: map[string]bool{"contended.example.com": true}}
|
||||
|
||||
@@ -338,6 +352,7 @@ func TestWorker_LeaseContention_OnlyOneNodeRenews(t *testing.T) {
|
||||
// path: a freshly issued certificate, nowhere near its renewal window, must
|
||||
// still reissue on demand.
|
||||
func TestForceRenewCertificate_BypassesRenewalWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
svc.TenantCA = &fakeCertAuthority{permitted: map[string]bool{"force.example.com": true}}
|
||||
|
||||
@@ -365,6 +380,7 @@ func TestForceRenewCertificate_BypassesRenewalWindow(t *testing.T) {
|
||||
// an imported certificate rather than silently no-op'ing or crashing — this
|
||||
// worker only ever drives PRIVATE_CA reissuance.
|
||||
func TestForceRenewCertificate_RejectsNonPrivateType(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c, k := genCert(t, "force-import.example.com")
|
||||
out, err := svc.ImportCertificate(context.Background(), &acm.ImportCertificateInput{Certificate: c, PrivateKey: k}, testAccountID)
|
||||
@@ -379,6 +395,7 @@ func TestForceRenewCertificate_RejectsNonPrivateType(t *testing.T) {
|
||||
// ownership check: an ARN that does not exist, or belongs to another
|
||||
// account, must be rejected rather than disclosing anything about it.
|
||||
func TestForceRenewCertificate_UnknownArnRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
worker := NewWorker(svc, "node-1")
|
||||
_, err := worker.ForceRenewCertificate(context.Background(), &ForceRenewCertificateInput{
|
||||
|
||||
@@ -67,6 +67,7 @@ func genCert(t *testing.T, cn string, dnsNames ...string) (certPEM, keyPEM []byt
|
||||
}
|
||||
|
||||
func TestImportCertificate_MintsArnAndParsesLeaf(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
certPEM, keyPEM := genCert(t, "example.com", "example.com", "www.example.com")
|
||||
|
||||
@@ -93,6 +94,7 @@ func TestImportCertificate_MintsArnAndParsesLeaf(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImportCertificate_MismatchedKeyRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
certPEM, _ := genCert(t, "a.example.com")
|
||||
_, otherKey := genCert(t, "b.example.com")
|
||||
@@ -106,6 +108,7 @@ func TestImportCertificate_MismatchedKeyRejected(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImportCertificate_GarbagePEMRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
_, err := svc.ImportCertificate(context.Background(), &acm.ImportCertificateInput{
|
||||
Certificate: []byte("not a pem"),
|
||||
@@ -116,6 +119,7 @@ func TestImportCertificate_GarbagePEMRejected(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListCertificates_ScopedToAccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c1, k1 := genCert(t, "one.example.com")
|
||||
c2, k2 := genCert(t, "two.example.com")
|
||||
@@ -132,6 +136,7 @@ func TestListCertificates_ScopedToAccount(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDescribeCertificate_CrossAccountHidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c1, k1 := genCert(t, "secret.example.com")
|
||||
out, err := svc.ImportCertificate(context.Background(), &acm.ImportCertificateInput{Certificate: c1, PrivateKey: k1}, testAccountID)
|
||||
@@ -145,6 +150,7 @@ func TestDescribeCertificate_CrossAccountHidden(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteCertificate(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c1, k1 := genCert(t, "del.example.com")
|
||||
out, err := svc.ImportCertificate(context.Background(), &acm.ImportCertificateInput{Certificate: c1, PrivateKey: k1}, testAccountID)
|
||||
@@ -165,6 +171,7 @@ func TestDeleteCertificate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImportCertificate_ReimportPreservesInUseBy(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c1, k1 := genCert(t, "inuse.example.com")
|
||||
|
||||
@@ -190,6 +197,7 @@ func TestImportCertificate_ReimportPreservesInUseBy(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImportCertificate_ReimportInvokesFanoutHook(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c1, k1 := genCert(t, "hook.example.com")
|
||||
|
||||
@@ -215,6 +223,7 @@ func TestImportCertificate_ReimportInvokesFanoutHook(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImportCertificate_ReimportZeroInUseBy_HookNotCalled(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c1, k1 := genCert(t, "unused.example.com")
|
||||
|
||||
@@ -240,6 +249,7 @@ func TestImportCertificate_ReimportZeroInUseBy_HookNotCalled(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImportCertificate_FreshImportDoesNotInvokeHook(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
called := false
|
||||
svc.CertMaterialUpdated = func(ctx context.Context, arn string) error {
|
||||
@@ -254,6 +264,7 @@ func TestImportCertificate_FreshImportDoesNotInvokeHook(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestImportCertificate_ReimportUnknownArnRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c1, k1 := genCert(t, "re.example.com")
|
||||
_, err := svc.ImportCertificate(context.Background(), &acm.ImportCertificateInput{
|
||||
@@ -270,6 +281,7 @@ func TestImportCertificate_ReimportUnknownArnRejected(t *testing.T) {
|
||||
// contain the plaintext PEM key, and reading it back through the Store must
|
||||
// still recover the original plaintext.
|
||||
func TestImportCertificate_PrivateKeyEncryptedAtRest(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
certPEM, keyPEM := genCert(t, "atrest.example.com")
|
||||
|
||||
@@ -297,6 +309,7 @@ func TestImportCertificate_PrivateKeyEncryptedAtRest(t *testing.T) {
|
||||
// record is stored. Precedent: TestSensitiveDataNotLogged_MasterKey in
|
||||
// handlers/iam/service_impl_test.go.
|
||||
func TestDescribeCertificate_DoesNotLeakPrivateKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
certPEM, keyPEM := genCert(t, "noleak.example.com")
|
||||
out, err := svc.ImportCertificate(context.Background(), &acm.ImportCertificateInput{
|
||||
@@ -370,6 +383,7 @@ func genLeafPEM(cn string, dnsNames []string) (certPEM, keyPEM string, err error
|
||||
}
|
||||
|
||||
func TestRequestCertificate_ProviderAPI_PendingValidationNoResourceRecord(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
svc.dnsProviderConfigured = true
|
||||
|
||||
@@ -395,6 +409,7 @@ func TestRequestCertificate_ProviderAPI_PendingValidationNoResourceRecord(t *tes
|
||||
}
|
||||
|
||||
func TestRequestCertificate_ManualTXT_EmitsTXTResourceRecordAndIneligible(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
svc.NorthstarHostsZone = func(domain string) bool { return true }
|
||||
|
||||
@@ -423,6 +438,7 @@ func TestRequestCertificate_ManualTXT_EmitsTXTResourceRecordAndIneligible(t *tes
|
||||
// PRIVATE_CA (automatic, offline, renewable), not MANUAL_TXT (which can never
|
||||
// complete against an internal zone a public CA cannot resolve).
|
||||
func TestRequestCertificate_PrivateCA_WinsOverNorthstarHostedZone(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
svc.TenantCA = &fakeCertAuthority{permitted: map[string]bool{"home.example.com": true}}
|
||||
svc.NorthstarHostsZone = func(domain string) bool { return domain == "home.example.com" }
|
||||
@@ -442,6 +458,7 @@ func TestRequestCertificate_PrivateCA_WinsOverNorthstarHostedZone(t *testing.T)
|
||||
}
|
||||
|
||||
func TestRequestCertificate_PrivateCA_IssuesSynchronously(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
svc.TenantCA = &fakeCertAuthority{permitted: map[string]bool{"private.example.com": true}}
|
||||
|
||||
@@ -470,6 +487,7 @@ func TestRequestCertificate_PrivateCA_IssuesSynchronously(t *testing.T) {
|
||||
// the daemon-wiring slice adds no coverage for the seam between
|
||||
// RequestCertificate and the real implementation without a test like this one.
|
||||
func TestRequestCertificate_PrivateCA_RealTenantCA_IssuesAndChains(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
dir := t.TempDir()
|
||||
ca, err := LoadOrCreateTenantCA(
|
||||
@@ -509,6 +527,7 @@ func TestRequestCertificate_PrivateCA_RealTenantCA_IssuesAndChains(t *testing.T)
|
||||
}
|
||||
|
||||
func TestRequestCertificate_PrivateCA_DomainOutsidePermittedRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
svc.TenantCA = &fakeCertAuthority{permitted: map[string]bool{"allowed.example.com": true}}
|
||||
|
||||
@@ -520,6 +539,7 @@ func TestRequestCertificate_PrivateCA_DomainOutsidePermittedRejected(t *testing.
|
||||
}
|
||||
|
||||
func TestRequestCertificate_PrivateCA_SANOutsidePermittedRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
svc.TenantCA = &fakeCertAuthority{permitted: map[string]bool{"allowed.example.com": true}}
|
||||
|
||||
@@ -532,6 +552,7 @@ func TestRequestCertificate_PrivateCA_SANOutsidePermittedRejected(t *testing.T)
|
||||
}
|
||||
|
||||
func TestRequestCertificate_PrivateCA_NoTenantCAWired_FailsLoudly(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
// TenantCA deliberately left nil: PRIVATE_CA is derived (no dns provider, no
|
||||
// northstar zone) but nothing can drive it.
|
||||
@@ -542,6 +563,7 @@ func TestRequestCertificate_PrivateCA_NoTenantCAWired_FailsLoudly(t *testing.T)
|
||||
}
|
||||
|
||||
func TestRequestCertificate_PrivateCA_IssueLeafErrorNotStored(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
svc.TenantCA = &fakeCertAuthority{
|
||||
permitted: map[string]bool{"broken.example.com": true},
|
||||
@@ -559,6 +581,7 @@ func TestRequestCertificate_PrivateCA_IssueLeafErrorNotStored(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRequestCertificate_NoDomainName_Rejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
_, err := svc.RequestCertificate(context.Background(), &acm.RequestCertificateInput{}, testAccountID)
|
||||
require.Error(t, err)
|
||||
@@ -566,6 +589,7 @@ func TestRequestCertificate_NoDomainName_Rejected(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRequestCertificate_PrivateCA_DescribeDoesNotLeakPrivateKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
svc.TenantCA = &fakeCertAuthority{permitted: map[string]bool{"noleak-priv.example.com": true}}
|
||||
|
||||
@@ -583,6 +607,7 @@ func TestRequestCertificate_PrivateCA_DescribeDoesNotLeakPrivateKey(t *testing.T
|
||||
}
|
||||
|
||||
func TestImportCertificate_RenewalEligibilityIneligible(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c1, k1 := genCert(t, "imported-ineligible.example.com")
|
||||
out, err := svc.ImportCertificate(context.Background(), &acm.ImportCertificateInput{Certificate: c1, PrivateKey: k1}, testAccountID)
|
||||
@@ -595,6 +620,7 @@ func TestImportCertificate_RenewalEligibilityIneligible(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteCertificate_RefusesWhenInUse(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c1, k1 := genCert(t, "inuse-delete.example.com")
|
||||
out, err := svc.ImportCertificate(context.Background(), &acm.ImportCertificateInput{Certificate: c1, PrivateKey: k1}, testAccountID)
|
||||
@@ -612,6 +638,7 @@ func TestDeleteCertificate_RefusesWhenInUse(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteCertificate_SucceedsWhenNotInUse(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c1, k1 := genCert(t, "notinuse-delete.example.com")
|
||||
out, err := svc.ImportCertificate(context.Background(), &acm.ImportCertificateInput{Certificate: c1, PrivateKey: k1}, testAccountID)
|
||||
@@ -625,6 +652,8 @@ func TestDeleteCertificate_SucceedsWhenNotInUse(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), awserrors.ErrorResourceNotFound)
|
||||
}
|
||||
|
||||
// Not parallel: it swaps the process-wide slog default, which parallel tests
|
||||
// would write to concurrently.
|
||||
func TestSensitiveDataNotLogged_ACMPrivateKey(t *testing.T) {
|
||||
var buf strings.Builder
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
@@ -654,6 +683,7 @@ func TestSensitiveDataNotLogged_ACMPrivateKey(t *testing.T) {
|
||||
// on one either: every spelling below names a real certificate under certKey's
|
||||
// old last-slash rule while resolving account-wide at the gate.
|
||||
func TestCertificateArnSpellingsTheGateCannotRead(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
certPEM, keyPEM := genCert(t, "fenced.example.com")
|
||||
|
||||
@@ -681,6 +711,7 @@ func TestCertificateArnSpellingsTheGateCannotRead(t *testing.T) {
|
||||
|
||||
for _, tt := range spellings {
|
||||
t.Run(tt.certArn, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := svc.DescribeCertificate(context.Background(),
|
||||
&acm.DescribeCertificateInput{CertificateArn: aws.String(tt.certArn)}, testAccountID)
|
||||
require.Error(t, err)
|
||||
@@ -710,6 +741,7 @@ func TestCertificateArnSpellingsTheGateCannotRead(t *testing.T) {
|
||||
// The handler mints the ARN the gate builds, or every resource-scoped statement
|
||||
// names an object that does not exist.
|
||||
func TestMintedArnRoundTripsThroughTheGatesFormatter(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
minted := svc.mintCertificateArn(testAccountID)
|
||||
|
||||
@@ -722,6 +754,7 @@ func TestMintedArnRoundTripsThroughTheGatesFormatter(t *testing.T) {
|
||||
// empty: the SDK marshaller drops a nil slice, so a caller that ranges over the
|
||||
// result without a nil check breaks on every account's first state.
|
||||
func TestListCertificates_EmptyAccountReturnsEmptyList(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
|
||||
list, err := svc.ListCertificates(context.Background(), &acm.ListCertificatesInput{}, testAccountID)
|
||||
@@ -737,6 +770,7 @@ func TestListCertificates_EmptyAccountReturnsEmptyList(t *testing.T) {
|
||||
// The populated case must keep working: an empty slice that never fills is just
|
||||
// a different way of losing the list.
|
||||
func TestListCertificates_PopulatedAccountReturnsSummaries(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c1, k1 := genCert(t, "filled-one.example.com")
|
||||
c2, k2 := genCert(t, "filled-two.example.com")
|
||||
@@ -760,6 +794,7 @@ func TestListCertificates_PopulatedAccountReturnsSummaries(t *testing.T) {
|
||||
// fault. InternalErrorException is a retryable 500, so Terraform sat on
|
||||
// "Still creating..." instead of failing; this asserts the 4xx and the reason.
|
||||
func TestRequestCertificate_NoTenantCAIsAClientErrorNamingTheCause(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
require.Nil(t, svc.TenantCA, "this test only means anything with no tenant CA wired")
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ func rawKV(t *testing.T, s *Store) jetstream.KeyValue {
|
||||
}
|
||||
|
||||
func TestAddInUseBy_AddsAndDedupes(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
rec := &CertRecord{CertificateArn: "arn:aws:acm:ap-southeast-2:000000000001:certificate/inuse-1", AccountID: testAccountID}
|
||||
require.NoError(t, store.PutCert(t.Context(), rec))
|
||||
@@ -62,6 +63,7 @@ func TestAddInUseBy_AddsAndDedupes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAddInUseBy_NoopOnMissingCert(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
// No cert exists under this ARN — must not error and must not create one.
|
||||
require.NoError(t, store.AddInUseBy(t.Context(), "arn:aws:acm:ap-southeast-2:000000000001:certificate/missing", "arn:lb/one"))
|
||||
@@ -74,6 +76,7 @@ func TestAddInUseBy_NoopOnMissingCert(t *testing.T) {
|
||||
// A certificate deleted after the ownership check must surface as not found
|
||||
// rather than a silent success, and must not be recreated by the tag write.
|
||||
func TestUpdateTags_MissingCertReturnsNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
const certArn = "arn:aws:acm:ap-southeast-2:000000000001:certificate/missing"
|
||||
called := false
|
||||
@@ -90,6 +93,7 @@ func TestUpdateTags_MissingCertReturnsNotFound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRemoveInUseBy_RemovesEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
rec := &CertRecord{
|
||||
CertificateArn: "arn:aws:acm:ap-southeast-2:000000000001:certificate/inuse-2",
|
||||
@@ -116,6 +120,7 @@ func TestRemoveInUseBy_RemovesEntry(t *testing.T) {
|
||||
// a healthy, renewed certificate. Only asserting the exact final set
|
||||
// catches that; asserting "no errors" does not.
|
||||
func TestAddInUseBy_ConcurrentDistinctResourcesAllSurvive(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
certArn := "arn:aws:acm:ap-southeast-2:000000000001:certificate/inuse-concurrent"
|
||||
require.NoError(t, store.PutCert(t.Context(), &CertRecord{CertificateArn: certArn, AccountID: testAccountID}))
|
||||
@@ -146,6 +151,7 @@ func TestAddInUseBy_ConcurrentDistinctResourcesAllSurvive(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRemoveInUseBy_NoopWhenAbsentOrMissingCert(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
rec := &CertRecord{
|
||||
CertificateArn: "arn:aws:acm:ap-southeast-2:000000000001:certificate/inuse-3",
|
||||
@@ -165,6 +171,7 @@ func TestRemoveInUseBy_NoopWhenAbsentOrMissingCert(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStore_PutCert_EncryptsPrivateKeyAtRest(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
const plaintext = "-----BEGIN EC PRIVATE KEY-----\nZm9v\n-----END EC PRIVATE KEY-----\n"
|
||||
arn := "arn:aws:acm:ap-southeast-2:000000000001:certificate/enc"
|
||||
@@ -186,6 +193,7 @@ func TestStore_PutCert_EncryptsPrivateKeyAtRest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStore_GetCert_LegacyPlaintextPassthroughAndReencrypt(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
const plaintext = "-----BEGIN RSA PRIVATE KEY-----\nYmFy\n-----END RSA PRIVATE KEY-----\n"
|
||||
arn := "arn:aws:acm:ap-southeast-2:000000000001:certificate/legacy"
|
||||
@@ -215,6 +223,7 @@ func TestStore_GetCert_LegacyPlaintextPassthroughAndReencrypt(t *testing.T) {
|
||||
// as a PEM private key block must be a hard error, never silently trusted as
|
||||
// legacy plaintext.
|
||||
func TestStore_GetCert_RejectsGarbageAsLegacyPlaintext(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
arn := "arn:aws:acm:ap-southeast-2:000000000001:certificate/garbage"
|
||||
|
||||
@@ -233,6 +242,7 @@ func TestStore_GetCert_RejectsGarbageAsLegacyPlaintext(t *testing.T) {
|
||||
// bucket would disagree silently, with the reader getting ciphertext where
|
||||
// it expects PEM.
|
||||
func TestNewStore_RequiresMasterKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
_, err := NewStore(t.Context(), nc, nil)
|
||||
require.Error(t, err)
|
||||
@@ -243,6 +253,7 @@ func TestNewStore_RequiresMasterKey(t *testing.T) {
|
||||
// delegation token, and the resumable ACME/lease state — survive a
|
||||
// PutCert/GetCert round trip unchanged, alongside the encrypted PrivateKey.
|
||||
func TestStore_PutGetCert_RoundTripsManagedIssuanceFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
arn := "arn:aws:acm:ap-southeast-2:000000000001:certificate/managed-1"
|
||||
rec := &CertRecord{
|
||||
@@ -284,6 +295,7 @@ func TestStore_PutGetCert_RoundTripsManagedIssuanceFields(t *testing.T) {
|
||||
// the renewal predicate never reads PrivateKey and the scan runs on every
|
||||
// tick for every certificate in the store.
|
||||
func TestListAllCertMetadata_DoesNotDecryptPrivateKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
const plaintext = "-----BEGIN EC PRIVATE KEY-----\nZm9v\n-----END EC PRIVATE KEY-----\n"
|
||||
arn := "arn:aws:acm:ap-southeast-2:000000000001:certificate/metadata-only"
|
||||
@@ -309,6 +321,7 @@ func TestListAllCertMetadata_DoesNotDecryptPrivateKey(t *testing.T) {
|
||||
// sees every account's certificates, unlike the account-scoped ListCerts used
|
||||
// by the ListCertificates API.
|
||||
func TestListAllCertMetadata_CrossesAccounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
require.NoError(t, store.PutCert(t.Context(), &CertRecord{
|
||||
CertificateArn: "arn:aws:acm:ap-southeast-2:1:certificate/acct-a", AccountID: "000000000001",
|
||||
@@ -328,6 +341,7 @@ func TestListAllCertMetadata_CrossesAccounts(t *testing.T) {
|
||||
// and that shape has already produced one silent-plaintext-write bug in this
|
||||
// package, so every write path over CertRecord gets this guard.
|
||||
func TestAcquireLease_LeavesPrivateKeyEncrypted(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
const plaintext = "-----BEGIN EC PRIVATE KEY-----\nZm9v\n-----END EC PRIVATE KEY-----\n"
|
||||
arn := "arn:aws:acm:ap-southeast-2:000000000001:certificate/lease-encrypted"
|
||||
@@ -353,6 +367,7 @@ func TestAcquireLease_LeavesPrivateKeyEncrypted(t *testing.T) {
|
||||
// TestReleaseLease_LeavesPrivateKeyEncrypted is AcquireLease's counterpart
|
||||
// for the release path — same raw-CAS shape, same guard.
|
||||
func TestReleaseLease_LeavesPrivateKeyEncrypted(t *testing.T) {
|
||||
t.Parallel()
|
||||
store := setupACMStore(t)
|
||||
const plaintext = "-----BEGIN EC PRIVATE KEY-----\nZm9v\n-----END EC PRIVATE KEY-----\n"
|
||||
arn := "arn:aws:acm:ap-southeast-2:000000000001:certificate/lease-release-encrypted"
|
||||
|
||||
@@ -24,6 +24,7 @@ func importTagged(t *testing.T, svc *ACMServiceImpl, tags ...*acm.Tag) string {
|
||||
}
|
||||
|
||||
func TestListTagsForCertificate_ReturnsImportTags(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
arn := importTagged(t, svc, &acm.Tag{Key: aws.String("Name"), Value: aws.String("ingress")})
|
||||
|
||||
@@ -37,6 +38,7 @@ func TestListTagsForCertificate_ReturnsImportTags(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListTagsForCertificate_NoTagsReturnsEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
arn := importTagged(t, svc)
|
||||
|
||||
@@ -48,6 +50,7 @@ func TestListTagsForCertificate_NoTagsReturnsEmpty(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAddTagsToCertificate_MergesAndPersists(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
arn := importTagged(t, svc, &acm.Tag{Key: aws.String("Name"), Value: aws.String("ingress")})
|
||||
|
||||
@@ -72,6 +75,7 @@ func TestAddTagsToCertificate_MergesAndPersists(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRemoveTagsFromCertificate_ByKeyAndByValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
arn := importTagged(t, svc,
|
||||
&acm.Tag{Key: aws.String("Name"), Value: aws.String("ingress")},
|
||||
@@ -100,6 +104,7 @@ func TestRemoveTagsFromCertificate_ByKeyAndByValue(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTagOps_CrossAccountHidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
arn := importTagged(t, svc, &acm.Tag{Key: aws.String("Name"), Value: aws.String("ingress")})
|
||||
|
||||
@@ -112,6 +117,7 @@ func TestTagOps_CrossAccountHidden(t *testing.T) {
|
||||
// Tag writes must change only the stored tag field: the encrypted key
|
||||
// survives byte-for-byte and still decrypts, and the tag change takes effect.
|
||||
func TestTagOps_PreserveStoredPrivateKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := map[string]struct {
|
||||
write func(svc *ACMServiceImpl, certArn string) error
|
||||
wantTags map[string]string
|
||||
@@ -139,6 +145,7 @@ func TestTagOps_PreserveStoredPrivateKey(t *testing.T) {
|
||||
}
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
certArn := importTagged(t, svc, &acm.Tag{Key: aws.String("Name"), Value: aws.String("ingress")})
|
||||
|
||||
@@ -189,6 +196,7 @@ func listTagMap(t *testing.T, svc *ACMServiceImpl, arn string) map[string]string
|
||||
// AWS answers a re-import carrying Tags with ValidationException and leaves
|
||||
// the stored certificate, material and tags alike, untouched.
|
||||
func TestImportCertificate_ReimportWithTagsRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
arn := importTagged(t, svc, &acm.Tag{Key: aws.String("Name"), Value: aws.String("ingress")})
|
||||
|
||||
@@ -216,6 +224,7 @@ func TestImportCertificate_ReimportWithTagsRejected(t *testing.T) {
|
||||
// A re-import without Tags keeps the tags already on the certificate rather
|
||||
// than clearing them, as InUseBy is kept.
|
||||
func TestImportCertificate_ReimportKeepsStoredTags(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
arn := importTagged(t, svc, &acm.Tag{Key: aws.String("Name"), Value: aws.String("ingress")})
|
||||
_, err := svc.AddTagsToCertificate(context.Background(), &acm.AddTagsToCertificateInput{
|
||||
@@ -242,6 +251,7 @@ func TestImportCertificate_ReimportKeepsStoredTags(t *testing.T) {
|
||||
// AWS resolves the ARN before refusing the tags, so an unknown ARN with tags is
|
||||
// a ResourceNotFoundException, not the tagging ValidationException.
|
||||
func TestImportCertificate_ReimportWithTagsUnknownArnIsNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := setupACMService(t)
|
||||
c, k := genCert(t, "x.example.com", "x.example.com")
|
||||
_, err := svc.ImportCertificate(context.Background(), &acm.ImportCertificateInput{
|
||||
|
||||
@@ -21,6 +21,7 @@ func hostsZoneFake(hosted ...string) func(string) bool {
|
||||
// deriveValidationMode, independent of RequestCertificate's surrounding
|
||||
// plumbing.
|
||||
func TestDeriveValidationMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
tenantCA := &fakeCertAuthority{permitted: map[string]bool{
|
||||
"private.example.com": true,
|
||||
}}
|
||||
@@ -93,6 +94,7 @@ func TestDeriveValidationMode(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := &ACMServiceImpl{
|
||||
TenantCA: tc.tenantCA,
|
||||
dnsProviderConfigured: tc.dnsProviderConfigured,
|
||||
|
||||
Reference in New Issue
Block a user