mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
test: run the rds suite in parallel (#806)
This commit is contained in:
@@ -125,6 +125,7 @@ func readRecord(t *testing.T, svc *Service) (DBInstanceRecord, string) {
|
||||
}
|
||||
|
||||
func TestGetDBBootstrapConfig_ServesTheFullBootMaterial(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
rec := defaultRecord()
|
||||
rec.DataVolumeID = "vol-data-01"
|
||||
@@ -195,6 +196,7 @@ func TestGetDBBootstrapConfig_FormatGrantRequiresExactCurrentIdentity(t *testing
|
||||
}
|
||||
|
||||
func TestGetDBBootstrapConfig_MintsFreshCertEachFetch(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
seedInstance(t, svc, defaultRecord())
|
||||
ctx := context.Background()
|
||||
@@ -221,6 +223,7 @@ func TestGetDBBootstrapConfig_MintsFreshCertEachFetch(t *testing.T) {
|
||||
// The SAN set is what makes sslmode=verify-full work. The IP is the required
|
||||
// one, because the endpoint is a bare IP wherever DNS is not configured.
|
||||
func TestGetDBBootstrapConfig_CertSANsCoverIPAndHostname(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
rec := defaultRecord()
|
||||
seedInstance(t, svc, rec)
|
||||
@@ -237,6 +240,7 @@ func TestGetDBBootstrapConfig_CertSANsCoverIPAndHostname(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetDBBootstrapConfig_NoDNSNameStillMintsIPOnlyCert(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
rec := defaultRecord()
|
||||
rec.DNSName = ""
|
||||
@@ -255,6 +259,7 @@ func TestGetDBBootstrapConfig_NoDNSNameStillMintsIPOnlyCert(t *testing.T) {
|
||||
// is refused at binding rather than here — so the fetch must still serve a
|
||||
// config and boot a database.
|
||||
func TestGetDBBootstrapConfig_NoCAStillServesConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
svc := NewService(nc, testRegion).WithDeps(Deps{MasterKey: testMasterKey})
|
||||
seedPending(t, svc, defaultRecord())
|
||||
@@ -267,6 +272,7 @@ func TestGetDBBootstrapConfig_NoCAStillServesConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetDBBootstrapConfig_ConfiguredCARequiresENIPrivateIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
rec := defaultRecord()
|
||||
rec.ENIPrivateIP = ""
|
||||
@@ -281,6 +287,7 @@ func TestGetDBBootstrapConfig_ConfiguredCARequiresENIPrivateIP(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetDBBootstrapConfig_PartialCAConfigurationFailsClosed(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
svc := NewService(nc, testRegion).WithDeps(Deps{
|
||||
CACertPath: "/etc/spinifex/ca.pem",
|
||||
@@ -300,6 +307,7 @@ func TestGetDBBootstrapConfig_PartialCAConfigurationFailsClosed(t *testing.T) {
|
||||
// ever read. The staged password is untouched, which is what makes the agent's
|
||||
// retry succeed once the CA is readable again.
|
||||
func TestGetDBBootstrapConfig_UnloadableCALeavesPasswordRecoverable(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
loadErr := errors.New("read CA key /etc/spinifex/ca.key: permission denied")
|
||||
broken := true
|
||||
@@ -333,6 +341,7 @@ func TestGetDBBootstrapConfig_UnloadableCALeavesPasswordRecoverable(t *testing.T
|
||||
// the whole problem. A replay has no race to lose: every caller bound to the
|
||||
// generation is served the same password and none of them writes.
|
||||
func TestGetDBBootstrapConfig_ConcurrentFetchesAllReplayTheSamePassword(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
payloadID := seedPending(t, svc, defaultRecord())
|
||||
_, beforeRaw := readRecord(t, svc)
|
||||
@@ -367,6 +376,7 @@ func TestGetDBBootstrapConfig_ConcurrentFetchesAllReplayTheSamePassword(t *testi
|
||||
}
|
||||
|
||||
func TestGetDBBootstrapConfig_UnknownInstance(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
_, err := svc.GetDBBootstrapConfig(context.Background(),
|
||||
&GetDBBootstrapConfigInput{DBInstanceIdentifier: "ghost", InstanceID: testInstance}, testAccountID)
|
||||
@@ -385,6 +395,7 @@ func parseCertPEM(t *testing.T, pemStr string) *x509.Certificate {
|
||||
}
|
||||
|
||||
func TestRegisterDBInstance_IdempotentAndRefreshesLastSeen(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
seedInstance(t, svc, defaultRecord())
|
||||
ctx := context.Background()
|
||||
@@ -416,6 +427,7 @@ func TestRegisterDBInstance_IdempotentAndRefreshesLastSeen(t *testing.T) {
|
||||
// A replace mints a new instance ID, which is a new registration rather than a
|
||||
// continuation of the old VM's.
|
||||
func TestRegisterDBInstance_NewVMResetsRegisteredAt(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
seedInstance(t, svc, defaultRecord())
|
||||
ctx := context.Background()
|
||||
@@ -434,6 +446,7 @@ func TestRegisterDBInstance_NewVMResetsRegisteredAt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRegisterDBInstance_UnknownInstance(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
_, err := svc.RegisterDBInstance(context.Background(),
|
||||
&RegisterDBInstanceInput{DBInstanceIdentifier: "ghost", InstanceID: testInstance}, testAccountID)
|
||||
@@ -444,6 +457,7 @@ func TestRegisterDBInstance_UnknownInstance(t *testing.T) {
|
||||
// Liveness is the hottest path in the service, so an unchanged beat must stay in
|
||||
// memory and only reach KV on the slower floor.
|
||||
func TestSubmitDBStateChange_PersistsOnChangeAndOnFloor(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
seedInstance(t, svc, defaultRecord())
|
||||
ctx := context.Background()
|
||||
@@ -479,6 +493,7 @@ func TestSubmitDBStateChange_PersistsOnChangeAndOnFloor(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSubmitDBStateChange_RecordsParameterRollback(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
rec := defaultRecord()
|
||||
rec.DBParameterGroupName = "customer-params"
|
||||
@@ -513,6 +528,7 @@ func TestSubmitDBStateChange_RecordsParameterRollback(t *testing.T) {
|
||||
// In-memory liveness is fresher than the record between persists, which is what
|
||||
// lets the reconciler judge staleness without reading KV every tick.
|
||||
func TestSubmitDBStateChange_LastSeenTracksUnpersistedBeats(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
seedInstance(t, svc, defaultRecord())
|
||||
ctx := context.Background()
|
||||
@@ -541,6 +557,7 @@ func TestSubmitDBStateChange_LastSeenTracksUnpersistedBeats(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSubmitDBStateChange_RejectsUnknownHealth(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
seedInstance(t, svc, defaultRecord())
|
||||
_, err := svc.SubmitDBStateChange(context.Background(), &SubmitDBStateChangeInput{
|
||||
@@ -551,6 +568,7 @@ func TestSubmitDBStateChange_RejectsUnknownHealth(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInstanceIndex_RoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ func TestParseARN_RejectsMalformedAndForeignARNs(t *testing.T) {
|
||||
// The registry is what makes an unregistered kind a rejection rather than an
|
||||
// empty tag list, so every kind the parser accepts has to be accounted for.
|
||||
func TestTaggableResources_CoverEveryResourceThatExists(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, kind := range []ResourceKind{ResourceKindDBInstance, ResourceKindDBSnapshot,
|
||||
ResourceKindDBSubnetGroup, ResourceKindDBParameterGroup} {
|
||||
_, ok := taggableResources[kind]
|
||||
|
||||
@@ -12,6 +12,7 @@ const (
|
||||
)
|
||||
|
||||
func TestARNBuilders(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "arn:aws:rds:ap-southeast-2:123456789012:db:orders-db",
|
||||
DBInstanceARN(testRegion, testAccountID, "orders-db"))
|
||||
assert.Equal(t, "arn:aws:rds:ap-southeast-2:123456789012:snapshot:orders-db-2026-07-24",
|
||||
|
||||
@@ -78,6 +78,7 @@ func retainingRecord(days int64) DBInstanceRecord {
|
||||
// while KV is unhealthy, and cluster-wide scope is leader-gated by the framework
|
||||
// rather than by anything here.
|
||||
func TestBackupRetentionReaper_IsAClusterWideReaper(t *testing.T) {
|
||||
t.Parallel()
|
||||
reaper := NewService(nil, testRegion).NewBackupRetentionReaper()
|
||||
assert.Equal(t, "rds-backup-retention", reaper.Class())
|
||||
assert.Equal(t, vm.ScopeClusterWide, reaper.Scope())
|
||||
@@ -88,6 +89,7 @@ func TestBackupRetentionReaper_IsAClusterWideReaper(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSweep_RemovesOnlyTheBackupsPastRetention(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
seedInstance(t, h.svc, retainingRecord(7))
|
||||
|
||||
@@ -113,6 +115,7 @@ func TestSweep_RemovesOnlyTheBackupsPastRetention(t *testing.T) {
|
||||
// If backup creation has been failing for a week, strict retention would delete
|
||||
// the whole backup set at the moment a backup matters most.
|
||||
func TestSweep_NeverDeletesTheNewestBackupWhileBackupsAreOn(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
seedInstance(t, h.svc, retainingRecord(1))
|
||||
|
||||
@@ -132,6 +135,7 @@ func TestSweep_NeverDeletesTheNewestBackupWhileBackupsAreOn(t *testing.T) {
|
||||
// A snapshot still being cut is not something the customer could restore from, so
|
||||
// keeping it instead of the newest available one would leave them with nothing.
|
||||
func TestSweep_KeepsTheNewestAvailableRatherThanTheNewest(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
seedInstance(t, h.svc, retainingRecord(1))
|
||||
|
||||
@@ -147,6 +151,7 @@ func TestSweep_KeepsTheNewestAvailableRatherThanTheNewest(t *testing.T) {
|
||||
// The point of turning automated backups off is to make the data volume
|
||||
// GC-eligible again, so the last one goes too.
|
||||
func TestSweep_RemovesEverythingWhenBackupsAreOff(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
seedInstance(t, h.svc, retainingRecord(0))
|
||||
|
||||
@@ -163,6 +168,7 @@ func TestSweep_RemovesEverythingWhenBackupsAreOff(t *testing.T) {
|
||||
// restore an instance that no longer exists, and the alternative is a data volume
|
||||
// pinned for good.
|
||||
func TestSweep_RemovesEverythingWhenTheInstanceIsGone(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
|
||||
newest := h.seedBackup(t, time.Hour)
|
||||
@@ -177,6 +183,7 @@ func TestSweep_RemovesEverythingWhenTheInstanceIsGone(t *testing.T) {
|
||||
// and the branch it would select deletes a live instance's whole backup set. The
|
||||
// bucket listing is the corroboration that stops it.
|
||||
func TestSweep_LeavesTheBackupSetWhenTheRecordCannotBeRead(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
seedInstance(t, h.svc, retainingRecord(7))
|
||||
newest := h.seedBackup(t, time.Hour)
|
||||
@@ -214,6 +221,7 @@ func (m missingKey) Get(ctx context.Context, key string) (jetstream.KeyValueEntr
|
||||
// skip rather than a failure: the next pass retries, and the index entry stays so
|
||||
// the backup is not lost from it.
|
||||
func TestSweep_SkipsABackupStillInUse(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
seedInstance(t, h.svc, retainingRecord(0))
|
||||
inUse := h.seedBackup(t, 2*oneDay)
|
||||
@@ -235,6 +243,7 @@ func TestSweep_SkipsABackupStillInUse(t *testing.T) {
|
||||
// A pass that under-collects is corrected by the next one two minutes later; a
|
||||
// pass that walks an unbounded number of snapshots is not.
|
||||
func TestSweep_IsBoundedPerPassAndResumes(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.svc = h.svc.WithDeps(Deps{
|
||||
LoadCA: newTestCA(t),
|
||||
@@ -261,6 +270,7 @@ func TestSweep_IsBoundedPerPassAndResumes(t *testing.T) {
|
||||
// cleared so the index stops naming it, and nothing is counted as reaped because
|
||||
// no data went with it.
|
||||
func TestSweep_DropsAnIndexEntryWhoseSnapshotIsGone(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
seedInstance(t, h.svc, retainingRecord(7))
|
||||
|
||||
@@ -281,6 +291,7 @@ func TestSweep_DropsAnIndexEntryWhoseSnapshotIsGone(t *testing.T) {
|
||||
// Every tenant's retention is enforced on the same pass, so one account's backlog
|
||||
// cannot leave another's unenforced.
|
||||
func TestSweep_CoversEveryAccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
const otherAccount = "210987654321"
|
||||
h := newSnapshotHarness(t, false)
|
||||
|
||||
@@ -301,6 +312,7 @@ func TestSweep_CoversEveryAccount(t *testing.T) {
|
||||
// A truncated listing must never read as "no accounts": that would leave every
|
||||
// tenant's retention unenforced while looking like a clean pass.
|
||||
func TestSweep_FailsWhenTheAccountBucketsCannotBeEnumerated(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := NewService(nil, testRegion).NewBackupRetentionReaper().Sweep(t.Context())
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -308,6 +320,7 @@ func TestSweep_FailsWhenTheAccountBucketsCannotBeEnumerated(t *testing.T) {
|
||||
// The backstop for a crash between the last DeleteDBSnapshot and the inline
|
||||
// volume delete that follows it: nothing else references the volume by then.
|
||||
func TestSweep_ReclaimsAnOrphanedRetainedVolume(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
kv, err := h.svc.bucket(t.Context(), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -331,6 +344,7 @@ func TestSweep_ReclaimsAnOrphanedRetainedVolume(t *testing.T) {
|
||||
// written with: a snapshot taken since would read as "nothing holds it", and one
|
||||
// deleted since would pin the volume for good.
|
||||
func TestSweep_ReChecksTheHoldersRatherThanTrustingTheRecord(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
kv, err := h.svc.bucket(t.Context(), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -356,6 +370,7 @@ func TestSweep_ReChecksTheHoldersRatherThanTrustingTheRecord(t *testing.T) {
|
||||
// "nobody". It is recorded and re-checked rather than failing the pass, so a
|
||||
// disagreement between the two indexes never wedges the sweep.
|
||||
func TestSweep_RetainsAVolumeTheVolumeStoreRefusesToRelease(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
kv, err := h.svc.bucket(t.Context(), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -378,6 +393,7 @@ func TestSweep_RetainsAVolumeTheVolumeStoreRefusesToRelease(t *testing.T) {
|
||||
// Turning automated backups off sweeps the set that exists rather than leaving it
|
||||
// to expire against a retention that is now zero.
|
||||
func TestModifyDBInstance_SweepsTheAutomatedBackupsWhenRetentionGoesToZero(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
seedInstance(t, h.svc, retainingRecord(7))
|
||||
swept := h.seedBackup(t, time.Hour)
|
||||
@@ -396,6 +412,7 @@ func TestModifyDBInstance_SweepsTheAutomatedBackupsWhenRetentionGoesToZero(t *te
|
||||
// An automated backup outliving its instance would pin the instance's data
|
||||
// volume for good, so teardown sweeps the set before releasing the volume.
|
||||
func TestDeleteDBInstance_SweepsTheAutomatedBackups(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
seedInstance(t, h.svc, availableRecord())
|
||||
swept := seedAutomatedBackup(t, h.svc, h.snaps, testAccountID, testDBID, oneDay, SnapshotStatusAvailable)
|
||||
|
||||
@@ -117,6 +117,7 @@ func TestParseDailyWindow_RejectsWhatAWSRejects(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseDailyWindow_AcceptsAWindowThatWrapsMidnight(t *testing.T) {
|
||||
t.Parallel()
|
||||
window, err := parseDailyWindow("PreferredBackupWindow", "23:45-00:15")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, windowSlot, window.length())
|
||||
@@ -155,6 +156,7 @@ func TestParseWeeklyWindow_RejectsWhatAWSRejects(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseWeeklyWindow_AcceptsAWindowThatWrapsTheWeek(t *testing.T) {
|
||||
t.Parallel()
|
||||
window, err := parseWeeklyWindow("PreferredMaintenanceWindow", "sat:23:45-sun:00:15")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, windowSlot, window.length())
|
||||
@@ -171,6 +173,7 @@ func TestParseWeeklyWindow_AcceptsAWindowThatWrapsTheWeek(t *testing.T) {
|
||||
// AWS refuses the pair rather than the individual window, because the two would
|
||||
// collide the week the maintenance window's day comes round.
|
||||
func TestValidateWindows_RejectsAnOverlappingPair(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := NewService(nil, testRegion)
|
||||
|
||||
_, _, err := svc.validateWindows(testDBID, "03:00-04:00", "mon:03:30-mon:04:30")
|
||||
@@ -187,6 +190,7 @@ func TestValidateWindows_RejectsAnOverlappingPair(t *testing.T) {
|
||||
// Canonical text, not the customer's: a describe has to read back the string a
|
||||
// later modify compares against, or every modify would look like a change.
|
||||
func TestValidateWindows_ReturnsTheCanonicalText(t *testing.T) {
|
||||
t.Parallel()
|
||||
backup, maintenance, err := NewService(nil, testRegion).
|
||||
validateWindows(testDBID, "03:00-04:00", "SUN:05:00-sun:06:00")
|
||||
require.NoError(t, err)
|
||||
@@ -198,6 +202,7 @@ func TestValidateWindows_ReturnsTheCanonicalText(t *testing.T) {
|
||||
// window that moved whenever the record was rewritten would back up at an hour
|
||||
// the customer never saw.
|
||||
func TestValidateWindows_AssignsStableNonOverlappingWindows(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := NewService(nil, testRegion)
|
||||
block := svc.backupWindowBlock()
|
||||
maintenanceBlock := svc.maintenanceWindowBlock()
|
||||
@@ -237,6 +242,7 @@ func TestValidateWindows_AssignsStableNonOverlappingWindows(t *testing.T) {
|
||||
// second one on top of it would fail the same identifier deterministically,
|
||||
// forever.
|
||||
func TestValidateWindows_AssignsAroundACustomerNamedWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := NewService(nil, testRegion)
|
||||
// Inside the other window's block, which is where a collision is possible at
|
||||
// all: an hour of the maintenance block covers two of its assignable slots.
|
||||
@@ -270,6 +276,7 @@ func TestValidateWindows_AssignsAroundACustomerNamedWindow(t *testing.T) {
|
||||
// A pair the customer named in full is still refused — the assignment moving out
|
||||
// of the way is for windows the platform chose, not for the ones it was given.
|
||||
func TestValidateWindows_StillRejectsAPairTheCustomerNamed(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, err := NewService(nil, testRegion).
|
||||
validateWindows(testDBID, "13:00-14:00", "wed:13:30-wed:14:30")
|
||||
require.Error(t, err)
|
||||
@@ -280,6 +287,7 @@ func TestValidateWindows_StillRejectsAPairTheCustomerNamed(t *testing.T) {
|
||||
// stored the other one derived it. Anything else would schedule the two passes
|
||||
// over each other on exactly the records that name one window.
|
||||
func TestResolvedWindows_AssignAroundTheStoredWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := NewService(nil, testRegion)
|
||||
|
||||
for i := range 200 {
|
||||
@@ -307,6 +315,7 @@ func assertWindowsDoNotOverlap(t *testing.T, id, backup, maintenance string) {
|
||||
// An operator's typo cannot be allowed to fail every create on the node, so the
|
||||
// built-in block is used and the fault is logged instead.
|
||||
func TestWindowBlock_FallsBackToTheBuiltInBlock(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := NewService(nil, testRegion).WithDeps(Deps{Backup: BackupPolicy{
|
||||
BackupWindowBlock: "not-a-window",
|
||||
MaintenanceWindowBlock: "19:00",
|
||||
@@ -322,6 +331,7 @@ func TestWindowBlock_FallsBackToTheBuiltInBlock(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateRetentionPeriod_BoundsTheRetention(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := NewService(nil, testRegion)
|
||||
require.NoError(t, svc.validateRetentionPeriod(0))
|
||||
require.NoError(t, svc.validateRetentionPeriod(defaultBackupRetentionCapDays))
|
||||
@@ -345,6 +355,7 @@ func TestValidateRetentionPeriod_BoundsTheRetention(t *testing.T) {
|
||||
// window across leader churn and daemon restarts, and what stops a missed window
|
||||
// from being backfilled.
|
||||
func TestBackupDue_FiresOncePerWindowAndNeverBackfills(t *testing.T) {
|
||||
t.Parallel()
|
||||
window, err := parseDailyWindow("PreferredBackupWindow", "03:00-03:30")
|
||||
require.NoError(t, err)
|
||||
svc := NewService(nil, testRegion)
|
||||
@@ -382,6 +393,7 @@ func TestBackupDue_FiresOncePerWindowAndNeverBackfills(t *testing.T) {
|
||||
// the backoff is what keeps a broken backup from re-quiescing the engine on every
|
||||
// two-minute pass.
|
||||
func TestBackupDue_PacesRetriesInsideTheWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
window, err := parseDailyWindow("PreferredBackupWindow", "03:00-03:30")
|
||||
require.NoError(t, err)
|
||||
svc := NewService(nil, testRegion)
|
||||
@@ -409,6 +421,7 @@ func TestBackupDue_PacesRetriesInsideTheWindow(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunBackupWindow_TakesIndexesAndStampsTheBackup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
seedInstance(t, h.svc, backupReadyRecord())
|
||||
|
||||
@@ -451,6 +464,7 @@ func TestRunBackupWindow_TakesIndexesAndStampsTheBackup(t *testing.T) {
|
||||
// The window fires once however many passes run inside it, which is what a
|
||||
// two-minute reconciler and a leader handover both look like from here.
|
||||
func TestRunBackupWindow_FiresOncePerWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
seedInstance(t, h.svc, backupReadyRecord())
|
||||
|
||||
@@ -463,6 +477,7 @@ func TestRunBackupWindow_FiresOncePerWindow(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunBackupWindow_DoesNotFireOutsideTheWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := backupReadyRecord()
|
||||
rec.PreferredBackupWindow = closedBackupWindow(time.Now().UTC())
|
||||
@@ -474,6 +489,7 @@ func TestRunBackupWindow_DoesNotFireOutsideTheWindow(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunBackupWindow_DoesNotFireWithBackupsDisabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := backupReadyRecord()
|
||||
rec.BackupRetentionPeriod = 0
|
||||
@@ -488,6 +504,7 @@ func TestRunBackupWindow_DoesNotFireWithBackupsDisabled(t *testing.T) {
|
||||
// the next window rather than this one. The skip is evented, because a customer
|
||||
// looking for last night's backup has to be able to see why there is none.
|
||||
func TestRunBackupWindow_SkipsAnInstanceItCannotBackUp(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := backupReadyRecord()
|
||||
rec.Status = StatusRebooting
|
||||
@@ -514,6 +531,7 @@ func TestRunBackupWindow_SkipsAnInstanceItCannotBackUp(t *testing.T) {
|
||||
// A failed backup is a backup fault, never an instance fault. The
|
||||
// database stays available and the failure is counted so it is visible.
|
||||
func TestRunBackupWindow_CountsAFailureAndLeavesTheInstanceAvailable(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
seedInstance(t, h.svc, backupReadyRecord())
|
||||
h.snaps.createErr = assert.AnError
|
||||
@@ -534,6 +552,7 @@ func TestRunBackupWindow_CountsAFailureAndLeavesTheInstanceAvailable(t *testing.
|
||||
// A stored window this plane cannot parse is corruption rather than a schedule:
|
||||
// firing on a guessed window would back up at an hour nobody asked for.
|
||||
func TestRunBackupWindow_IgnoresAMalformedStoredWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := backupReadyRecord()
|
||||
rec.PreferredBackupWindow = "0300-0400"
|
||||
@@ -549,6 +568,7 @@ func TestRunBackupWindow_IgnoresAMalformedStoredWindow(t *testing.T) {
|
||||
// still carries none after it: reporting one back would show as drift in the next
|
||||
// configuration read for a request that never set it.
|
||||
func TestModifyDBInstance_DoesNotPersistTheWindowItWasNotGiven(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := retainingRecord(7)
|
||||
rec.PreferredMaintenanceWindow = ""
|
||||
@@ -569,6 +589,7 @@ func TestModifyDBInstance_DoesNotPersistTheWindowItWasNotGiven(t *testing.T) {
|
||||
// A record written before window fields existed carries no window at all, and still has to be
|
||||
// backed up — on the window a describe reports for it.
|
||||
func TestResolvedBackupWindow_DerivesAWindowForARecordWithoutOne(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := NewService(nil, testRegion)
|
||||
rec := &DBInstanceRecord{DBInstanceIdentifier: testDBID}
|
||||
|
||||
@@ -591,6 +612,7 @@ func TestResolvedBackupWindow_DerivesAWindowForARecordWithoutOne(t *testing.T) {
|
||||
// rather than never. Only the transition happens here; the apply itself is the
|
||||
// reconciler's single drain through applyPendingModifications.
|
||||
func TestRunMaintenanceWindow_OpensADeferredModify(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := backupReadyRecord()
|
||||
rec.FormatAuthorized = true
|
||||
@@ -647,6 +669,7 @@ func TestRunMaintenanceWindow_StandsDownWhenThereIsNothingToApply(t *testing.T)
|
||||
}
|
||||
|
||||
func TestRunMaintenanceWindow_OpensOncePerWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := backupReadyRecord()
|
||||
rec.PreferredMaintenanceWindow = openMaintenanceWindow(time.Now().UTC())
|
||||
@@ -664,6 +687,7 @@ func TestRunMaintenanceWindow_OpensOncePerWindow(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAutomatedSnapshotIdentifier_TakesAWSsOwnName(t *testing.T) {
|
||||
t.Parallel()
|
||||
at := time.Date(2026, 7, 30, 3, 25, 45, 0, time.UTC)
|
||||
assert.Equal(t, "rds:orders-db-2026-07-30-03-25", AutomatedSnapshotIdentifier(testDBID, at))
|
||||
// Minute-precise, so a retry inside the same window never collides with the
|
||||
@@ -678,6 +702,7 @@ func TestAutomatedSnapshotIdentifier_TakesAWSsOwnName(t *testing.T) {
|
||||
// Automated backups own the rds: namespace, as in AWS, so a customer snapshot can
|
||||
// never collide with one the scheduler mints.
|
||||
func TestCreateDBSnapshot_RejectsTheAutomatedNamespace(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshotSource(t)
|
||||
|
||||
@@ -692,6 +717,7 @@ func TestCreateDBSnapshot_RejectsTheAutomatedNamespace(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDescribeDBInstanceAutomatedBackups_ReportsTheBackupSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
seedInstance(t, h.svc, backupReadyRecord())
|
||||
|
||||
@@ -723,6 +749,7 @@ func TestDescribeDBInstanceAutomatedBackups_ReportsTheBackupSet(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDescribeDBInstanceAutomatedBackups_SkipsAnInstanceWithBackupsOff(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := backupReadyRecord()
|
||||
rec.BackupRetentionPeriod = 0
|
||||
@@ -735,6 +762,7 @@ func TestDescribeDBInstanceAutomatedBackups_SkipsAnInstanceWithBackupsOff(t *tes
|
||||
}
|
||||
|
||||
func TestDescribeDBInstanceAutomatedBackups_RejectsAnUnknownInstance(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
|
||||
_, err := h.svc.DescribeDBInstanceAutomatedBackups(t.Context(),
|
||||
@@ -768,6 +796,7 @@ func TestDescribeDBInstanceAutomatedBackups_RejectsUnimplementedFilters(t *testi
|
||||
// The window pass has to be part of the leader's own account loop: a backup
|
||||
// nothing calls is a backup that is never taken.
|
||||
func TestReconciler_RunsTheBackupWindowOnItsAccountPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := backupReadyRecord()
|
||||
now := time.Now().UTC()
|
||||
@@ -784,6 +813,7 @@ func TestReconciler_RunsTheBackupWindowOnItsAccountPass(t *testing.T) {
|
||||
// The GC framework's cluster-wide gate is the reconciler's own lease. Without it
|
||||
// the retention sweep would delete customer data from every node at once.
|
||||
func TestReconciler_ClusterLeaseFollowsLeadership(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t)
|
||||
|
||||
release, ok := h.rec.AcquireClusterLease()
|
||||
@@ -804,6 +834,7 @@ func TestReconciler_ClusterLeaseFollowsLeadership(t *testing.T) {
|
||||
// Automated backups are on by default at the full cap: a shorter default would
|
||||
// pay the whole storage cost of retaining a snapshot for a fraction of the cover.
|
||||
func TestValidateCreateRequest_DefaultsTheBackupSettings(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newCreateValidator()
|
||||
req, err := svc.validateCreateRequest(validCreateInput())
|
||||
require.NoError(t, err)
|
||||
@@ -816,6 +847,7 @@ func TestValidateCreateRequest_DefaultsTheBackupSettings(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateCreateRequest_AcceptsTheBackupSettings(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := validCreateInput()
|
||||
input.BackupRetentionPeriod = aws.Int64(3)
|
||||
input.PreferredBackupWindow = aws.String("03:00-04:00")
|
||||
@@ -866,6 +898,7 @@ func TestValidateCreateRequest_RejectsBadBackupSettings(t *testing.T) {
|
||||
// The window is persisted at create rather than derived on every read, so it
|
||||
// survives a change to the configured block the way AWS's assigned window does.
|
||||
func TestCreateDBInstance_PersistsTheBackupSettings(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
|
||||
_, err := h.svc.CreateDBInstance(t.Context(), validCreateInput(), testAccountID)
|
||||
@@ -884,6 +917,7 @@ func TestCreateDBInstance_PersistsTheBackupSettings(t *testing.T) {
|
||||
// Retention 0 is an answer, not an absence: a client that sees no
|
||||
// BackupRetentionPeriod cannot tell "backups are off" from "not reported".
|
||||
func TestDescribeDBInstances_ReportsTheBackupSettingsIncludingZero(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
rec := seedCreated(t, h, testDBInstanceID)
|
||||
|
||||
@@ -910,6 +944,7 @@ func TestDescribeDBInstances_ReportsTheBackupSettingsIncludingZero(t *testing.T)
|
||||
// Restoring from last night's backup is the whole point of keeping it, so the
|
||||
// namespace refused wherever a name is minted has to be accepted as a reference.
|
||||
func TestRestoreDBInstanceFromDBSnapshot_RestoresFromAnAutomatedBackup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := backupReadyRecord()
|
||||
// Everything a restore reproduces from the snapshot alone.
|
||||
@@ -943,6 +978,7 @@ func TestRestoreDBInstanceFromDBSnapshot_RestoresFromAnAutomatedBackup(t *testin
|
||||
// Only the retention sweep removes an automated backup, as in AWS: a customer
|
||||
// delete-db-snapshot against one is refused rather than silently accepted.
|
||||
func TestDeleteDBSnapshot_RefusesAnAutomatedBackup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
seedInstance(t, h.svc, backupReadyRecord())
|
||||
require.True(t, h.runBackupPass(t))
|
||||
|
||||
@@ -55,6 +55,7 @@ func storedPayload(t *testing.T, svc *Service, id string) (*BootstrapPayloadEnve
|
||||
}
|
||||
|
||||
func TestBootstrapPayload_RoundTripsAndCarriesNoCleartext(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
rec := defaultRecord()
|
||||
seedPending(t, svc, rec)
|
||||
@@ -76,6 +77,7 @@ func TestBootstrapPayload_RoundTripsAndCarriesNoCleartext(t *testing.T) {
|
||||
// The same password staged twice must not produce the same bytes, or a KV reader
|
||||
// could tell two instances share a password without decrypting either.
|
||||
func TestBootstrapPayload_CiphertextIsNotDeterministic(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
rec := defaultRecord()
|
||||
seedInstance(t, svc, rec)
|
||||
@@ -95,6 +97,7 @@ func TestBootstrapPayload_CiphertextIsNotDeterministic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBootstrapPayload_TamperedCiphertextIsRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
rec := defaultRecord()
|
||||
seedPending(t, svc, rec)
|
||||
@@ -118,6 +121,7 @@ func TestBootstrapPayload_TamperedCiphertextIsRejected(t *testing.T) {
|
||||
// A payload written under another master key must say so rather than surfacing a
|
||||
// raw GCM authentication failure, which reads as tampering.
|
||||
func TestBootstrapPayload_WrongMasterKeyIsReportedLegibly(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
rec := defaultRecord()
|
||||
seedPending(t, svc, rec)
|
||||
@@ -176,6 +180,7 @@ func TestBootstrapPayload_ClaimsBindItToOneInstanceGeneration(t *testing.T) {
|
||||
// The whole point of the split: a fetch serves the password without writing
|
||||
// anything, so a lost reply is recovered by asking again.
|
||||
func TestGetDBBootstrapConfig_ReplaysUntilAcknowledged(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
rec := defaultRecord()
|
||||
payloadID := seedPending(t, svc, rec)
|
||||
@@ -295,6 +300,7 @@ func TestAcknowledgeDBBootstrap_Resolution(t *testing.T) {
|
||||
// The operation that destroys key material must not depend on being able to read
|
||||
// it, or a master.key change would strand a bootstrapped instance forever.
|
||||
func TestAcknowledgeDBBootstrap_NeverDecrypts(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
payloadID := seedPending(t, svc, defaultRecord())
|
||||
|
||||
@@ -314,6 +320,7 @@ func TestAcknowledgeDBBootstrap_NeverDecrypts(t *testing.T) {
|
||||
// The volume ID is an echo of what the fetch handed the guest rather than
|
||||
// independent proof, but a mismatch is a platform bug worth denying on.
|
||||
func TestAcknowledgeDBBootstrap_RejectsAForeignDataVolume(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
payloadID := seedPending(t, svc, defaultRecord())
|
||||
|
||||
@@ -334,6 +341,7 @@ func TestAcknowledgeDBBootstrap_RejectsAForeignDataVolume(t *testing.T) {
|
||||
// A beta record whose password was already spent keeps attaching: it is serving
|
||||
// real data and holds no secret at rest to fix.
|
||||
func TestGetDBBootstrapConfig_LegacyConsumedRecordAttaches(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
rec := defaultRecord()
|
||||
rec.Bootstrap = BootstrapState{Consumed: true, ResolvedParameters: rec.Bootstrap.ResolvedParameters}
|
||||
@@ -353,6 +361,7 @@ func TestGetDBBootstrapConfig_LegacyConsumedRecordAttaches(t *testing.T) {
|
||||
// datadir to save. The password is removed on sight rather than left waiting for
|
||||
// an operator who may never act.
|
||||
func TestGetDBBootstrapConfig_LegacyPlaintextIsScrubbedIdempotently(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
rec := defaultRecord()
|
||||
rec.Bootstrap.MasterUserPassword = "beta-plaintext-pw"
|
||||
@@ -375,6 +384,7 @@ func TestGetDBBootstrapConfig_LegacyPlaintextIsScrubbedIdempotently(t *testing.T
|
||||
// registers and beats and the operator sees a running-but-broken instance
|
||||
// instead of a VM retrying into silence.
|
||||
func TestGetDBBootstrapConfig_UnreadablePayloadAttachesAndReportsWhy(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
seedPending(t, svc, defaultRecord())
|
||||
|
||||
@@ -391,6 +401,7 @@ func TestGetDBBootstrapConfig_UnreadablePayloadAttachesAndReportsWhy(t *testing.
|
||||
}
|
||||
|
||||
func TestCreateDBInstance_StagesThePasswordEncrypted(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
_, err := h.svc.CreateDBInstance(t.Context(), validCreateInput(), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -419,6 +430,7 @@ func TestCreateDBInstance_StagesThePasswordEncrypted(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateDBInstance_WithoutAMasterKeyStagesNothing(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
h.svc = h.svc.WithDeps(Deps{
|
||||
LoadCA: newTestCA(t),
|
||||
@@ -443,6 +455,7 @@ func TestCreateDBInstance_WithoutAMasterKeyStagesNothing(t *testing.T) {
|
||||
// is staged for it and an acknowledgement against it is denied rather than read
|
||||
// as a benign duplicate.
|
||||
func TestRestoredRecord_IsBornWithoutAStagedBootstrap(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
rec := defaultRecord()
|
||||
rec.Bootstrap.State = BootstrapStateNone
|
||||
@@ -459,6 +472,7 @@ func TestRestoredRecord_IsBornWithoutAStagedBootstrap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteDBInstance_RemovesTheStagedPayload(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
rec := availableRecord()
|
||||
seedPending(t, h.svc, rec)
|
||||
@@ -477,6 +491,7 @@ func TestDeleteDBInstance_RemovesTheStagedPayload(t *testing.T) {
|
||||
// The replacement would lose the format grant only the initial create can hold,
|
||||
// so it could never come up whatever the password did.
|
||||
func TestModifyDBInstance_RejectsADisruptiveChangeWhileTheBootstrapIsStaged(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifiableRecord()
|
||||
rec.Status = StatusFailed
|
||||
@@ -544,6 +559,7 @@ func TestReplaceInstanceVM_DiscardsAStagedBootstrap(t *testing.T) {
|
||||
// unusable: the create that follows owns the reservation, so the orphan it finds
|
||||
// is its own to overwrite.
|
||||
func TestCreateDBInstance_StagesOverAnOrphanedPayload(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
rec := defaultRecord()
|
||||
orphaned := seedPending(t, svc, rec)
|
||||
@@ -560,6 +576,7 @@ func TestCreateDBInstance_StagesOverAnOrphanedPayload(t *testing.T) {
|
||||
// the record's FailureReason: the status machine clears that on every
|
||||
// transition, which is exactly what a timed-out create does next.
|
||||
func TestMarkBootstrapUnrecoverable_ReasonSurvivesAStatusTransition(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifiableRecord()
|
||||
rec.Status = StatusCreating
|
||||
@@ -581,6 +598,7 @@ func TestMarkBootstrapUnrecoverable_ReasonSurvivesAStatusTransition(t *testing.T
|
||||
// Both reasons reach the customer, and an instance still holding staged
|
||||
// credentials while it serves is visible without a second call.
|
||||
func TestProjectDBInstance_ReportsTheBootstrapState(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := NewService(nil, testRegion)
|
||||
rec := defaultRecord()
|
||||
rec.Status = StatusAvailable
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
// The password is handed straight to the agent and never persisted, so the
|
||||
// command is the only place it exists between the request and the engine.
|
||||
func TestSetMasterPassword_CarriesTheCredentialsToTheAgent(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
|
||||
require.NoError(t, h.svc.setMasterPassword(t.Context(), testAccountID, testDBID, "postgres", "n3w-pw"))
|
||||
@@ -27,6 +28,7 @@ func TestSetMasterPassword_CarriesTheCredentialsToTheAgent(t *testing.T) {
|
||||
// A rotation that could not be applied has to fail loudly: reporting success
|
||||
// would leave the customer with a password the engine does not accept.
|
||||
func TestSetMasterPassword_FailsWhenTheAgentRejectsIt(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, true)
|
||||
|
||||
err := h.svc.setMasterPassword(t.Context(), testAccountID, testDBID, "postgres", "n3w-pw")
|
||||
@@ -37,6 +39,7 @@ func TestSetMasterPassword_FailsWhenTheAgentRejectsIt(t *testing.T) {
|
||||
// The settings the engine accepted but will not honour until it restarts
|
||||
// come back as the reply message, which is what a later reboot then clears.
|
||||
func TestApplyParameters_ReportsWhatIsPendingARestart(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
h.agent.replyWith("shared_buffers, max_connections")
|
||||
|
||||
@@ -53,6 +56,7 @@ func TestApplyParameters_ReportsWhatIsPendingARestart(t *testing.T) {
|
||||
|
||||
// An agent that reports nothing pending has applied everything live.
|
||||
func TestApplyParameters_ReportsNothingPendingOnAnEmptyReply(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
|
||||
pending, err := h.svc.applyParameters(t.Context(), testAccountID, testDBID,
|
||||
@@ -64,6 +68,7 @@ func TestApplyParameters_ReportsNothingPendingOnAnEmptyReply(t *testing.T) {
|
||||
// The reply is matched on the command ID, so a stale answer to an earlier
|
||||
// issuer cannot be mistaken for the outcome of this command.
|
||||
func TestIssueCommand_CorrelatesTheReplyByCommandID(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
|
||||
reply, err := h.svc.issueCommand(t.Context(), testAccountID, testDBID,
|
||||
|
||||
@@ -225,6 +225,7 @@ func validCreateInput() *rds.CreateDBInstanceInput {
|
||||
}
|
||||
|
||||
func TestCreateDBInstance_ProvisionsAndRecordsTheInstance(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
out, err := h.svc.CreateDBInstance(t.Context(), validCreateInput(), testAccountID)
|
||||
@@ -290,6 +291,7 @@ func TestCreateDBInstance_ProvisionsAndRecordsTheInstance(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateDBInstance_IAMFailurePrecedesReservationAndLaunch(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
iamErr := errors.New("IAM store unavailable")
|
||||
h.iam.PutRolePolicyErr = iamErr
|
||||
@@ -305,6 +307,7 @@ func TestCreateDBInstance_IAMFailurePrecedesReservationAndLaunch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateDBInstance_PublishesEndpointRecord(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
_, err := h.svc.CreateDBInstance(t.Context(), validCreateInput(), testAccountID)
|
||||
@@ -326,6 +329,7 @@ func TestCreateDBInstance_PublishesEndpointRecord(t *testing.T) {
|
||||
// Without northstar there is no name to resolve, so the endpoint has to be the
|
||||
// ENI IP itself rather than an empty host the client would dial.
|
||||
func TestCreateDBInstance_EndpointFallsBackToENIIPWithoutBaseDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
|
||||
out, err := h.svc.CreateDBInstance(t.Context(), validCreateInput(), testAccountID)
|
||||
@@ -346,6 +350,7 @@ func TestCreateDBInstance_EndpointFallsBackToENIIPWithoutBaseDomain(t *testing.T
|
||||
}
|
||||
|
||||
func TestCreateDBInstance_DuplicateIdentifierIsRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
|
||||
_, err := h.svc.CreateDBInstance(t.Context(), validCreateInput(), testAccountID)
|
||||
@@ -365,6 +370,7 @@ func TestCreateDBInstance_DuplicateIdentifierIsRejected(t *testing.T) {
|
||||
// written must withdraw it — otherwise the name is permanently unusable while
|
||||
// naming an instance that was never provisioned.
|
||||
func TestCreateDBInstance_LaunchFailureWithdrawsTheReservation(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
h.launch.launcher.err = errors.New("no host has capacity")
|
||||
|
||||
@@ -382,6 +388,7 @@ func TestCreateDBInstance_LaunchFailureWithdrawsTheReservation(t *testing.T) {
|
||||
// built. The deferred record delete removes the only thing naming the VM, its
|
||||
// two ENIs and the data volume, so anything left behind is unreclaimable.
|
||||
func TestCreateDBInstance_FailureAfterLaunchUnwindsEveryResource(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
|
||||
// Dropping the reserved record while the launch is in flight fails the
|
||||
@@ -403,6 +410,7 @@ func TestCreateDBInstance_FailureAfterLaunchUnwindsEveryResource(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateDBInstance_IndexFailureWithdrawsTheRecordedLaunch(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
// This invalid KV key makes the instance-index write fail after recordLaunch
|
||||
// has advanced the reservation revision.
|
||||
@@ -418,6 +426,7 @@ func TestCreateDBInstance_IndexFailureWithdrawsTheRecordedLaunch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateDBInstance_RecordLaunchDoesNotOverwriteAConcurrentReplacement(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
var replacement DBInstanceRecord
|
||||
h.launch.launcher.onLaunch = func() {
|
||||
@@ -431,6 +440,7 @@ func TestCreateDBInstance_RecordLaunchDoesNotOverwriteAConcurrentReplacement(t *
|
||||
}
|
||||
|
||||
func TestCreateDBInstance_RollbackDoesNotDeleteAConcurrentReplacement(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
h.launch.launcher.instanceID = "invalid instance id"
|
||||
var replacement DBInstanceRecord
|
||||
@@ -447,6 +457,7 @@ func TestCreateDBInstance_RollbackDoesNotDeleteAConcurrentReplacement(t *testing
|
||||
}
|
||||
|
||||
func TestCreateDBInstance_RollbackFollowsSameOwnerUpdates(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
h.launch.launcher.instanceID = "invalid instance id"
|
||||
h.launch.launcher.onTerminate = func() {
|
||||
@@ -470,6 +481,7 @@ func TestCreateDBInstance_RollbackFollowsSameOwnerUpdates(t *testing.T) {
|
||||
// The request may not be answered with an instance whose storage is
|
||||
// not actually encrypted, so an unencrypted volume fails the whole create.
|
||||
func TestCreateDBInstance_UnencryptedDataVolumeFailsTheCreate(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
h.launch.volumes.encrypted = false
|
||||
|
||||
@@ -480,6 +492,7 @@ func TestCreateDBInstance_UnencryptedDataVolumeFailsTheCreate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateDBInstance_SecurityGroupsMustBeInThePlacementVPC(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
|
||||
input := validCreateInput()
|
||||
@@ -500,6 +513,7 @@ func TestCreateDBInstance_SecurityGroupsMustBeInThePlacementVPC(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateDBInstance_RequiresADefaultVPCWithASubnet(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
h.network.vpcs = nil
|
||||
|
||||
@@ -560,6 +574,7 @@ func TestValidateCreateRequest_RejectsUnimplementedParameters(t *testing.T) {
|
||||
// DeletionProtection is honoured, so it has to be accepted
|
||||
// at create and carried onto the record DeleteDBInstance checks.
|
||||
func TestValidateCreateRequest_AcceptsDeletionProtection(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := validCreateInput()
|
||||
input.DeletionProtection = aws.Bool(true)
|
||||
|
||||
@@ -613,6 +628,7 @@ func TestValidateCreateRequest_RejectsMalformedRequests(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateCreateRequest_AcceptsTheImplicitDefaultParameterGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := validCreateInput()
|
||||
input.DBParameterGroupName = aws.String("default.postgres18")
|
||||
input.Port = aws.Int64(6543)
|
||||
@@ -628,6 +644,7 @@ func TestValidateCreateRequest_AcceptsTheImplicitDefaultParameterGroup(t *testin
|
||||
// The inert parameters are accepted as no-ops rather than rejected: omitting
|
||||
// them creates no false guarantee, so a Terraform config carrying them works.
|
||||
func TestValidateCreateRequest_AcceptsInertParameters(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := validCreateInput()
|
||||
input.AutoMinorVersionUpgrade = aws.Bool(true)
|
||||
input.CopyTagsToSnapshot = aws.Bool(true)
|
||||
|
||||
@@ -51,6 +51,7 @@ func skipFinalSnapshot() *rds.DeleteDBInstanceInput {
|
||||
// The whole teardown: the VM, the data volume, both NICs, the reverse index and
|
||||
// the record itself.
|
||||
func TestDeleteDBInstance_TearsDownEverythingItOwns(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
seedInstance(t, h.svc, availableRecord())
|
||||
require.NoError(t, h.svc.PutInstanceIndex(t.Context(), testInstance, InstanceIndexEntry{
|
||||
@@ -106,6 +107,7 @@ func TestDeleteDBInstance_RequiresAnExplicitSnapshotChoice(t *testing.T) {
|
||||
// The flag exists to stop exactly this call, so honouring it has to happen
|
||||
// before anything is torn down.
|
||||
func TestDeleteDBInstance_HonoursDeletionProtection(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
rec := availableRecord()
|
||||
rec.DeletionProtection = true
|
||||
@@ -123,6 +125,7 @@ func TestDeleteDBInstance_HonoursDeletionProtection(t *testing.T) {
|
||||
// The snapshot is taken once the VM is gone, so it reads a sealed data
|
||||
// volume rather than one a live engine is still writing to.
|
||||
func TestDeleteDBInstance_TakesTheFinalSnapshotAfterTheEngineAndVMAreDown(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
seedInstance(t, h.svc, availableRecord())
|
||||
|
||||
@@ -152,6 +155,7 @@ func TestDeleteDBInstance_TakesTheFinalSnapshotAfterTheEngineAndVMAreDown(t *tes
|
||||
}
|
||||
|
||||
func TestDeleteDBInstance_ReservesTheFinalSnapshotBeforeCuttingIt(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
seedInstance(t, h.svc, availableRecord())
|
||||
|
||||
@@ -173,6 +177,7 @@ func TestDeleteDBInstance_ReservesTheFinalSnapshotBeforeCuttingIt(t *testing.T)
|
||||
// creating. A retry adopts that exact account-scoped snapshot instead of
|
||||
// cutting another one and permanently pinning the source volume twice.
|
||||
func TestDeleteDBInstance_AdoptsAnInterruptedFinalSnapshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
rec := availableRecord()
|
||||
rec.Status = StatusDeleting
|
||||
@@ -211,6 +216,7 @@ func TestDeleteDBInstance_AdoptsAnInterruptedFinalSnapshot(t *testing.T) {
|
||||
// be deleted while the final snapshot survives. It is retained and recorded
|
||||
// with the snapshots holding it.
|
||||
func TestDeleteDBInstance_RetainsTheVolumeAFinalSnapshotStillHolds(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
seedInstance(t, h.svc, availableRecord())
|
||||
|
||||
@@ -231,6 +237,7 @@ func TestDeleteDBInstance_RetainsTheVolumeAFinalSnapshotStillHolds(t *testing.T)
|
||||
// re-created under the same name must not have a stale snapshot of a previous
|
||||
// data volume accepted as its own — the volume would then be deleted unbacked.
|
||||
func TestDeleteDBInstance_RejectsAFinalSnapshotIdentifierAlreadyTaken(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
seedInstance(t, h.svc, availableRecord())
|
||||
|
||||
@@ -296,6 +303,7 @@ func TestDeleteDBInstance_RejectsARetryThatChangesTheSnapshotChoice(t *testing.T
|
||||
// reference the EC2 enumeration missed. Retaining converges; erroring would
|
||||
// wedge the delete on every retry until the bound marked it failed.
|
||||
func TestDeleteDBInstance_RetainsTheVolumeWhenTheVolumeStoreReportsItInUse(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
h.volumes.deleteErr = errors.New(awserrors.ErrorVolumeInUse)
|
||||
seedInstance(t, h.svc, availableRecord())
|
||||
@@ -316,6 +324,7 @@ func TestDeleteDBInstance_RetainsTheVolumeWhenTheVolumeStoreReportsItInUse(t *te
|
||||
// A teardown that died partway through is replayed, so every step has to treat
|
||||
// a resource that is already gone as work it has done.
|
||||
func TestDeleteDBInstance_IsIdempotentAcrossARetriedTeardown(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
rec := availableRecord()
|
||||
// The shape a delete leaves behind when its caller died mid-teardown.
|
||||
@@ -342,6 +351,7 @@ func TestDeleteDBInstance_IsIdempotentAcrossARetriedTeardown(t *testing.T) {
|
||||
// The reconciler owns a teardown whose caller never came back, so an
|
||||
// interrupted delete finishes without an operator.
|
||||
func TestReconciler_ResumesAnInterruptedDelete(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
rec := availableRecord()
|
||||
rec.Status = StatusDeleting
|
||||
@@ -357,6 +367,7 @@ func TestReconciler_ResumesAnInterruptedDelete(t *testing.T) {
|
||||
// A stop whose caller died leaves the VM possibly still running, so the stop is
|
||||
// re-issued rather than assumed.
|
||||
func TestReconciler_ResumesAnInterruptedStop(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
rec := availableRecord()
|
||||
rec.Status = StatusStopping
|
||||
|
||||
@@ -23,6 +23,7 @@ func seedCreated(t *testing.T, h *createHarness, id string) DBInstanceRecord {
|
||||
}
|
||||
|
||||
func TestDescribeDBInstances_ListsEveryInstanceSorted(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
for _, id := range []string{"zulu-db", "alpha-db", "mike-db"} {
|
||||
seedCreated(t, h, id)
|
||||
@@ -40,6 +41,7 @@ func TestDescribeDBInstances_ListsEveryInstanceSorted(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDescribeDBInstances_EmptyAccountIsAnEmptyList(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
|
||||
out, err := h.svc.DescribeDBInstances(t.Context(), nil, testAccountID)
|
||||
@@ -50,6 +52,7 @@ func TestDescribeDBInstances_EmptyAccountIsAnEmptyList(t *testing.T) {
|
||||
// A client polling a create must be able to tell "not ready" from "gone", so a
|
||||
// named instance that does not exist is an error rather than an empty list.
|
||||
func TestDescribeDBInstances_NamedMissingInstanceIsAnError(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
seedCreated(t, h, testDBInstanceID)
|
||||
|
||||
@@ -61,6 +64,7 @@ func TestDescribeDBInstances_NamedMissingInstanceIsAnError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDescribeDBInstances_NamedInstanceProjectsTheRecord(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
rec := seedCreated(t, h, testDBInstanceID)
|
||||
|
||||
@@ -94,6 +98,7 @@ func TestDescribeDBInstances_NamedInstanceProjectsTheRecord(t *testing.T) {
|
||||
// identifier, so an instance created without one is unmanageable by the tool
|
||||
// this service is driven with — its post-create read finds nothing.
|
||||
func TestDescribeDBInstances_ReportsAStableResourceID(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
rec := seedCreated(t, h, testDBInstanceID)
|
||||
require.Regexp(t, `^db-[0-9a-f]{17}$`, rec.DbiResourceID)
|
||||
@@ -137,6 +142,7 @@ func TestDescribeDBInstances_FiltersOnResourceIDAndIdentifier(t *testing.T) {
|
||||
// false would leave every default configuration with a diff that no modify can
|
||||
// clear.
|
||||
func TestDescribeDBInstances_EchoesAutoMinorVersionUpgrade(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
|
||||
unset := seedCreated(t, h, testDBInstanceID)
|
||||
@@ -157,6 +163,7 @@ func TestDescribeDBInstances_EchoesAutoMinorVersionUpgrade(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDescribeDBInstances_EchoesAcceptedInertSettings(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
input := validCreateInput()
|
||||
input.CopyTagsToSnapshot = aws.Bool(true)
|
||||
@@ -182,6 +189,7 @@ func TestDescribeDBInstances_EchoesAcceptedInertSettings(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDescribeDBInstances_EchoesOmittedInertSettingsAsDisabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
seedCreated(t, h, testDBInstanceID)
|
||||
|
||||
@@ -198,6 +206,7 @@ func TestDescribeDBInstances_EchoesOmittedInertSettingsAsDisabled(t *testing.T)
|
||||
// A filter nobody implemented is refused rather than dropped: dropping it
|
||||
// returns exactly the rows the caller asked to exclude.
|
||||
func TestDescribeDBInstances_RejectsAnUnrecognizedFilter(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
seedCreated(t, h, testDBInstanceID)
|
||||
|
||||
@@ -211,6 +220,7 @@ func TestDescribeDBInstances_RejectsAnUnrecognizedFilter(t *testing.T) {
|
||||
// A named instance and a filter that excludes it disagree; reporting the
|
||||
// instance anyway would answer a question the caller did not ask.
|
||||
func TestDescribeDBInstances_NamedInstanceMustSatisfyTheFilters(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
seedCreated(t, h, testDBInstanceID)
|
||||
|
||||
@@ -227,6 +237,7 @@ func TestDescribeDBInstances_NamedInstanceMustSatisfyTheFilters(t *testing.T) {
|
||||
// Instances live in per-account buckets, so one account's describe must not see
|
||||
// another's — the identifier is only unique within an account.
|
||||
func TestDescribeDBInstances_IsScopedToTheCallingAccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
seedCreated(t, h, testDBInstanceID)
|
||||
|
||||
@@ -238,6 +249,7 @@ func TestDescribeDBInstances_IsScopedToTheCallingAccount(t *testing.T) {
|
||||
// A DB instance with no endpoint yet has no Endpoint at all: an Endpoint with an
|
||||
// empty address would have a client dial an empty host.
|
||||
func TestProjectDBInstance_OmitsAnUnsettledEndpoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
|
||||
instance := h.svc.projectDBInstance(&DBInstanceRecord{
|
||||
@@ -253,6 +265,7 @@ func TestProjectDBInstance_OmitsAnUnsettledEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDesiredDNSChanges_CoversEveryTenantOrClaimsNoAuthority(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
rec := seedCreated(t, h, testDBInstanceID)
|
||||
|
||||
@@ -267,6 +280,7 @@ func TestDesiredDNSChanges_CoversEveryTenantOrClaimsNoAuthority(t *testing.T) {
|
||||
// Without a base domain there are no managed RDS records, and claiming authority
|
||||
// would let the reconcile prune records this node cannot even name.
|
||||
func TestDesiredDNSChanges_ClaimsNoAuthorityWithoutABaseDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
seedCreated(t, h, testDBInstanceID)
|
||||
|
||||
@@ -279,6 +293,7 @@ func TestDesiredDNSChanges_ClaimsNoAuthorityWithoutABaseDomain(t *testing.T) {
|
||||
// anything still holding an ENI IP keeps its name resolvable, including a failed
|
||||
// instance an operator is still investigating.
|
||||
func TestDesiredDNSChanges_SkipsDeletedButKeepsFailed(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
seedCreated(t, h, "gone-db")
|
||||
failed := seedCreated(t, h, "failed-db")
|
||||
@@ -301,6 +316,7 @@ func TestDesiredDNSChanges_SkipsDeletedButKeepsFailed(t *testing.T) {
|
||||
// A deferred change is what the customer polls for, and the Terraform provider
|
||||
// reads PendingModifiedValues to decide whether an apply has landed.
|
||||
func TestProjectDBInstance_ReportsTheDeferredChange(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
|
||||
instance := h.svc.projectDBInstance(&DBInstanceRecord{
|
||||
@@ -326,6 +342,7 @@ func TestProjectDBInstance_ReportsTheDeferredChange(t *testing.T) {
|
||||
// so reporting it as still pending would show Terraform drift on a change that
|
||||
// has landed.
|
||||
func TestProjectDBInstance_OmitsAPendingFilesystemGrow(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
|
||||
instance := h.svc.projectDBInstance(&DBInstanceRecord{
|
||||
@@ -340,6 +357,7 @@ func TestProjectDBInstance_OmitsAPendingFilesystemGrow(t *testing.T) {
|
||||
// AWS reports a parameter group's state on the membership rather than in
|
||||
// PendingModifiedValues, and the Terraform provider reads it there.
|
||||
func TestProjectDBInstance_ReportsTheParameterGroupApplyStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
rec := &DBInstanceRecord{
|
||||
DBInstanceIdentifier: testDBInstanceID,
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
// they did, a create would have resolved an AMI nothing builds and left a volume
|
||||
// and an ENI behind for an instance that could never become available.
|
||||
func TestMariaDB_IsOffered(t *testing.T) {
|
||||
t.Parallel()
|
||||
engine, err := LookupEngine(" MariaDB ")
|
||||
require.NoError(t, err, "the engine name is matched case-insensitively and trimmed")
|
||||
assert.Equal(t, engineMariaDB.Name, engine.Name)
|
||||
@@ -40,6 +41,7 @@ func TestMariaDB_IsOffered(t *testing.T) {
|
||||
// the MariaDB AMI carrying MariaDB's own port, family and guest configuration,
|
||||
// not PostgreSQL's defaults with a different name on them.
|
||||
func TestCreateDBInstance_LaunchesMariaDB(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
input := validCreateInput()
|
||||
input.Engine = aws.String("mariadb")
|
||||
@@ -75,6 +77,7 @@ func TestCreateDBInstance_LaunchesMariaDB(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMariaDB_Identity(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "mariadb", engineMariaDB.Name)
|
||||
assert.Equal(t, int64(3306), engineMariaDB.DefaultPort)
|
||||
// The version series the API pins to, not an integer major: PostgreSQL's
|
||||
@@ -125,6 +128,7 @@ func TestMariaDB_ValidateMasterUsername(t *testing.T) {
|
||||
// The guest re-checks the name it is handed rather than trusting the control
|
||||
// plane, and it reaches the reserved half on its own.
|
||||
func TestMariaDB_ValidateUsernameNotReserved(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, username := range []string{"mysql.session", "MySQL.infoschema", "mariadb.sys", "mariadb.anything"} {
|
||||
err := engineMariaDB.ValidateUsernameNotReserved(username)
|
||||
require.Error(t, err, "username %q", username)
|
||||
@@ -137,6 +141,7 @@ func TestMariaDB_ValidateUsernameNotReserved(t *testing.T) {
|
||||
// MariaDB maps a database onto a directory name and rds-init interpolates the
|
||||
// name into CREATE DATABASE from the shell, so this rule is the actual barrier.
|
||||
func TestMariaDB_ValidateDBName(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, name := range []string{"", "orders", "Orders_2", strings.Repeat("d", 64)} {
|
||||
assert.NoError(t, engineMariaDB.ValidateDBName(name), "name %q", name)
|
||||
}
|
||||
@@ -154,6 +159,7 @@ func TestMariaDB_ValidateDBName(t *testing.T) {
|
||||
// The two catalogs are separate tables, not one table with an engine column, so
|
||||
// neither engine can be handed a setting the other's server would not parse.
|
||||
func TestMariaDBCatalog_IsSeparateFromPostgres(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, name := range []string{"innodb_buffer_pool_size", "sql_mode", "key_buffer_size"} {
|
||||
_, ok := enginePostgres.LookupParameter(name)
|
||||
assert.False(t, ok, "PostgreSQL must not expose %s", name)
|
||||
@@ -173,6 +179,7 @@ func TestMariaDBCatalog_IsSeparateFromPostgres(t *testing.T) {
|
||||
// customer's to set: the endpoint, the agent's socket access, the serving
|
||||
// certificate and the snapshot recovery guarantee all depend on them.
|
||||
func TestMariaDBCatalog_OmitsPlatformOwnedSettings(t *testing.T) {
|
||||
t.Parallel()
|
||||
owned := []string{
|
||||
"port", "datadir", "socket", "bind_address",
|
||||
"ssl_ca", "ssl_cert", "ssl_key",
|
||||
@@ -189,6 +196,7 @@ func TestMariaDBCatalog_OmitsPlatformOwnedSettings(t *testing.T) {
|
||||
// The size-derived defaults have to move with the class, or the formulas are
|
||||
// decoration and one end of the supported range is mis-tuned.
|
||||
func TestMariaDBCatalog_SizeDerivedDefaultsScaleWithClassMemory(t *testing.T) {
|
||||
t.Parallel()
|
||||
smallest, err := classMemoryMiB("db.t3.micro")
|
||||
require.NoError(t, err)
|
||||
largest, err := classMemoryMiB("db.m5.xlarge")
|
||||
@@ -212,6 +220,7 @@ func TestMariaDBCatalog_SizeDerivedDefaultsScaleWithClassMemory(t *testing.T) {
|
||||
// Three quarters of class memory is RDS's own default for this engine family,
|
||||
// and it is the setting whose being wrong stops the server from starting.
|
||||
func TestMariaDBCatalog_BufferPoolIsThreeQuartersOfClassMemory(t *testing.T) {
|
||||
t.Parallel()
|
||||
memoryMiB, err := classMemoryMiB("db.m5.large")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -226,6 +235,7 @@ func TestMariaDBCatalog_BufferPoolIsThreeQuartersOfClassMemory(t *testing.T) {
|
||||
|
||||
// max_connections = {DBInstanceClassMemory/12582880}, which is RDS's own formula.
|
||||
func TestMariaDBCatalog_MaxConnectionsFollowsTheRDSFormula(t *testing.T) {
|
||||
t.Parallel()
|
||||
memoryMiB, err := classMemoryMiB("db.m5.xlarge")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, strconv.FormatInt(memoryMiB*mibToBytes/12582880, 10), mariadbMaxConnectionsFor(memoryMiB))
|
||||
@@ -377,6 +387,7 @@ func TestMariaDBCatalog_BoundsSizeDerivedOverridesToTheClass(t *testing.T) {
|
||||
// installed file. Getting one wrong is a permanent pending-reboot or a change
|
||||
// the customer is told took effect and did not.
|
||||
func TestMariaDBCatalog_ApplyTypesMatchTheEngine(t *testing.T) {
|
||||
t.Parallel()
|
||||
static := []string{
|
||||
"innodb_buffer_pool_size", "innodb_log_file_size", "innodb_flush_method",
|
||||
"innodb_read_io_threads", "innodb_write_io_threads", "innodb_purge_threads",
|
||||
@@ -401,6 +412,7 @@ func TestMariaDBCatalog_ApplyTypesMatchTheEngine(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMariaDBCatalog_ResolvesEveryParameterAsALiteral(t *testing.T) {
|
||||
t.Parallel()
|
||||
resolved, err := engineMariaDB.ResolveEffectiveParameters("db.m5.large", map[string]string{"max_connections": "300"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resolved, len(engineMariaDB.CatalogParameterNames()))
|
||||
@@ -426,6 +438,7 @@ func TestMariaDBCatalog_ResolvesEveryParameterAsALiteral(t *testing.T) {
|
||||
// time_zone startup option, so an option file naming it aborts the server before
|
||||
// it opens the datadir.
|
||||
func TestMariaDBCatalog_TimeZoneUsesItsStartupSpellingInTheOptionFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "default_time_zone", engineMariaDB.OptionFileName("time_zone"))
|
||||
|
||||
// The customer-facing name is what the API reports and what a live SET GLOBAL
|
||||
@@ -437,6 +450,7 @@ func TestMariaDBCatalog_TimeZoneUsesItsStartupSpellingInTheOptionFile(t *testing
|
||||
}
|
||||
|
||||
func TestMariaDBCatalog_EveryOtherParameterKeepsItsName(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, name := range engineMariaDB.CatalogParameterNames() {
|
||||
if name == "time_zone" {
|
||||
continue
|
||||
@@ -449,6 +463,7 @@ func TestMariaDBCatalog_EveryOtherParameterKeepsItsName(t *testing.T) {
|
||||
// PostgreSQL sets every one of its settings under the name the customer knows,
|
||||
// so a mapping there would be a bug rather than a fix.
|
||||
func TestPostgresCatalog_NoParameterIsRenamedForTheOptionFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, name := range enginePostgres.CatalogParameterNames() {
|
||||
assert.Equal(t, name, enginePostgres.OptionFileName(name))
|
||||
}
|
||||
@@ -457,5 +472,6 @@ func TestPostgresCatalog_NoParameterIsRenamedForTheOptionFile(t *testing.T) {
|
||||
// A name the catalog does not carry is the customer's own, and the option file
|
||||
// takes it unchanged rather than silently dropping it.
|
||||
func TestMariaDBCatalog_OptionFileNamePassesAnUnknownNameThrough(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "not_a_parameter", engineMariaDB.OptionFileName("not_a_parameter"))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestLookupEngine(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := LookupEngine("PostgreSQL")
|
||||
require.Error(t, err, "the AWS engine identifier is 'postgres', not the product name")
|
||||
|
||||
@@ -28,6 +29,7 @@ func TestLookupEngine(t *testing.T) {
|
||||
// A version other than the pinned one would be served by an image that is
|
||||
// not the one asked for, so it is rejected rather than quietly substituted.
|
||||
func TestEngineValidateVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
engine, err := LookupEngine("postgres")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -109,6 +111,7 @@ func TestEngineValidateDBName(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateMasterUserPassword(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.NoError(t, ValidateMasterUserPassword("Sup3rSecret!"))
|
||||
assert.NoError(t, ValidateMasterUserPassword(strings.Repeat("x", maxMasterPasswordLen)))
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ func describeEvents(t *testing.T, svc *Service, input *rds.DescribeEventsInput)
|
||||
}
|
||||
|
||||
func TestDescribeEvents_ReturnsARecordedEventWithItsSourceARN(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
svc.RecordEvent(context.Background(), testAccountID, EventSourceTypeDBInstance, testDBID,
|
||||
"DB instance stopped.", EventCategoryAvailability)
|
||||
@@ -57,6 +58,7 @@ func TestDescribeEvents_ReturnsARecordedEventWithItsSourceARN(t *testing.T) {
|
||||
// AWS's default window is one hour, so an event outside it is not returned even
|
||||
// though it is still inside the retention window.
|
||||
func TestDescribeEvents_ScopesToTheRequestedWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
now := time.Now().UTC()
|
||||
seedEvent(t, svc, EventSourceTypeDBInstance, testDBID, now.Add(-10*time.Minute), "recent")
|
||||
@@ -69,6 +71,7 @@ func TestDescribeEvents_ScopesToTheRequestedWindow(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDescribeEvents_FiltersByCategory(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
now := time.Now().UTC()
|
||||
seedEvent(t, svc, EventSourceTypeDBInstance, testDBID, now.Add(-time.Minute), "backed up", EventCategoryBackup)
|
||||
@@ -83,6 +86,7 @@ func TestDescribeEvents_FiltersByCategory(t *testing.T) {
|
||||
// An unfiltered read is account-wide, so a resource's history is reachable even
|
||||
// after the resource itself is gone.
|
||||
func TestDescribeEvents_ReadsAcrossResourcesAndScopesToOneWhenAsked(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
now := time.Now().UTC()
|
||||
seedEvent(t, svc, EventSourceTypeDBInstance, testDBID, now, "instance event")
|
||||
@@ -131,6 +135,7 @@ func TestDescribeEvents_RejectsMalformedRequests(t *testing.T) {
|
||||
// The ring is bounded, so a resource's whole history stays one KV value however
|
||||
// long it lives.
|
||||
func TestAppendEvent_BoundsTheRing(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
for range eventRingSize + 10 {
|
||||
require.NoError(t, svc.appendEvent(context.Background(), testAccountID,
|
||||
@@ -149,6 +154,7 @@ func TestAppendEvent_BoundsTheRing(t *testing.T) {
|
||||
// AWS's 14-day retention: anything older is dropped on the next append and
|
||||
// never returned by a read.
|
||||
func TestAppendEvent_DropsEventsPastTheRetentionWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := newTestService(t)
|
||||
seedEvent(t, svc, EventSourceTypeDBInstance, testDBID,
|
||||
time.Now().UTC().Add(-EventRetention-time.Hour), "ancient")
|
||||
@@ -163,6 +169,7 @@ func TestAppendEvent_DropsEventsPastTheRetentionWindow(t *testing.T) {
|
||||
// An event narrates work that already happened, so a failed append must not
|
||||
// turn a successful operation into a failed one.
|
||||
func TestRecordEvent_DoesNotPropagateAFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := NewService(nil, testRegion)
|
||||
assert.NotPanics(t, func() {
|
||||
svc.RecordEvent(context.Background(), testAccountID, EventSourceTypeDBInstance, testDBID, "stopped")
|
||||
|
||||
@@ -27,6 +27,7 @@ func rdsInstanceProfileARN(accountID string) string {
|
||||
// A Postgres RCE on a DB VM inherits this role, so the grant must be the four
|
||||
// internal actions and nothing wildcarded that could reach the customer surface.
|
||||
func TestInstanceRolePolicy_GrantsOnlyTheInternalActions(t *testing.T) {
|
||||
t.Parallel()
|
||||
var doc handlers_iam.PolicyDocument
|
||||
require.NoError(t, json.Unmarshal([]byte(instanceRoleInlinePolicy), &doc))
|
||||
require.Len(t, doc.Statement, 1)
|
||||
@@ -52,6 +53,7 @@ func TestInstanceRolePolicy_GrantsOnlyTheInternalActions(t *testing.T) {
|
||||
// under the VM's own account, so a role in the customer account would be
|
||||
// invisible to the agent and it would get no credentials at all.
|
||||
func TestEnsureInstanceProfile_CreatesInSystemAccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
f := iammock.New()
|
||||
arn, err := ensureInstanceProfile(func() handlers_iam.SystemInstanceRoleEnsurer { return f }, utils.GlobalAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -66,6 +68,7 @@ func TestEnsureInstanceProfile_CreatesInSystemAccount(t *testing.T) {
|
||||
// Every launch calls this, so a second call must converge on the existing role
|
||||
// rather than create a second one, and must re-assert the same grant.
|
||||
func TestEnsureInstanceProfile_IsIdempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
f := iammock.New()
|
||||
provider := func() handlers_iam.SystemInstanceRoleEnsurer { return f }
|
||||
|
||||
@@ -102,6 +105,7 @@ func TestEnsureInstanceProfile_RejectsUnavailableIAM(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEnsureInstanceProfile_PreservesEnsureFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
ensureErr := errors.New("IAM storage unavailable")
|
||||
f := iammock.New()
|
||||
f.PutRolePolicyErr = ensureErr
|
||||
@@ -116,6 +120,7 @@ func TestEnsureInstanceProfile_PreservesEnsureFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEnsureInstanceProfile_RejectsEmptyARN(t *testing.T) {
|
||||
t.Parallel()
|
||||
f := iammock.New()
|
||||
f.EmptyInstanceProfileARN = true
|
||||
|
||||
|
||||
@@ -417,6 +417,7 @@ func TestNATSVolumeAttacher_MapsNoResponderByInstanceState(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNATSVolumeAttacher_StoppedLookupFailureIsInternal(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
|
||||
attacher := NewNATSVolumeAttacher(nc)
|
||||
@@ -426,6 +427,7 @@ func TestNATSVolumeAttacher_StoppedLookupFailureIsInternal(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNATSVolumeAttacher_RejectsAnEmptyAttachment(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, nc := testutil.StartTestNATS(t)
|
||||
_, err := nc.Subscribe("ec2.cmd."+testInstance, func(msg *nats.Msg) {
|
||||
_ = msg.Respond([]byte(`{}`))
|
||||
@@ -511,6 +513,7 @@ func tagOf(specs []*ec2.TagSpecification, key string) string {
|
||||
// --- Tests ---
|
||||
|
||||
func TestLaunchDBInstanceVMWiresBothNICs(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLaunchHarness()
|
||||
|
||||
out, err := LaunchDBInstanceVM(t.Context(), h.deps(), testLaunchInput())
|
||||
@@ -564,6 +567,7 @@ func TestLaunchDBInstanceVMWiresBothNICs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLaunchDBInstanceVMAttachesTheDataVolume(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLaunchHarness()
|
||||
|
||||
out, err := LaunchDBInstanceVM(t.Context(), h.deps(), testLaunchInput())
|
||||
@@ -591,6 +595,7 @@ func TestLaunchDBInstanceVMAttachesTheDataVolume(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLaunchDBInstanceVMReportsExistingVolumeIntentAndSerial(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLaunchHarness()
|
||||
in := testLaunchInput()
|
||||
in.ExistingDataVolume = "vol-existing-01"
|
||||
@@ -652,6 +657,7 @@ func TestLaunchDBInstanceVMRollsBackEverythingItCreated(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLaunchDBInstanceVMTerminatesTheVMBeforeReleasingWhatIsAttachedToIt(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLaunchHarness()
|
||||
h.attacher.err = errors.New("no free hot-plug port")
|
||||
|
||||
@@ -665,6 +671,7 @@ func TestLaunchDBInstanceVMTerminatesTheVMBeforeReleasingWhatIsAttachedToIt(t *t
|
||||
}
|
||||
|
||||
func TestLaunchDBInstanceVMRollsBackAfterTheCallersContextIsDone(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLaunchHarness()
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
@@ -705,6 +712,7 @@ func TestLaunchDBInstanceVMRejectsIncompleteInput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveEngineAMIMatchesOnManifestTags(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLaunchHarness()
|
||||
|
||||
amiID, err := resolveEngineAMI(t.Context(), h.images, "postgres", "18")
|
||||
@@ -726,6 +734,7 @@ func TestResolveEngineAMIMatchesOnManifestTags(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveEngineAMIOmitsTheVersionFilterWhenUnset(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLaunchHarness()
|
||||
|
||||
_, err := resolveEngineAMI(t.Context(), h.images, "postgres", "")
|
||||
@@ -737,6 +746,7 @@ func TestResolveEngineAMIOmitsTheVersionFilterWhenUnset(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveEngineAMIPicksTheNewestBuild(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLaunchHarness()
|
||||
h.images.images = []*ec2.Image{
|
||||
{ImageId: aws.String("ami-old"), CreationDate: aws.String("2025-06-01T00:00:00Z")},
|
||||
@@ -750,6 +760,7 @@ func TestResolveEngineAMIPicksTheNewestBuild(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveEngineAMISkipsMalformedCatalogEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLaunchHarness()
|
||||
h.images.images = []*ec2.Image{
|
||||
nil,
|
||||
@@ -764,6 +775,7 @@ func TestResolveEngineAMISkipsMalformedCatalogEntries(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveEngineAMIExcludesGPUTaggedBuilds(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLaunchHarness()
|
||||
h.images.images = []*ec2.Image{
|
||||
{ImageId: aws.String(testEngineAMI), CreationDate: aws.String("2026-01-02T00:00:00Z")},
|
||||
@@ -781,6 +793,7 @@ func TestResolveEngineAMIExcludesGPUTaggedBuilds(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveEngineAMIFailsWhenOnlyGPUBuildsMatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLaunchHarness()
|
||||
h.images.images = []*ec2.Image{{
|
||||
ImageId: aws.String("ami-rds-postgres-18-gpu"),
|
||||
@@ -794,6 +807,7 @@ func TestResolveEngineAMIFailsWhenOnlyGPUBuildsMatch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLaunchDBInstanceVMFailsWithoutAnEngineAMI(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLaunchHarness()
|
||||
h.images.images = nil
|
||||
|
||||
|
||||
@@ -310,6 +310,7 @@ func (h *lifecycleHarness) events(t *testing.T) []Event {
|
||||
// The engine is asked to stop cleanly first, so the reboot is a restart of a
|
||||
// checkpointed cluster rather than a crash it has to replay.
|
||||
func TestRebootDBInstance_StopsTheEngineThenTheVM(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
rec := availableRecord()
|
||||
rec.FormatAuthorized = true
|
||||
@@ -335,6 +336,7 @@ func TestRebootDBInstance_StopsTheEngineThenTheVM(t *testing.T) {
|
||||
// The static parameters are already in the engine's config, so the restart
|
||||
// is what applies them and the record stops advertising them.
|
||||
func TestRebootDBInstance_ClearsThePendingRebootParameters(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
rec := availableRecord()
|
||||
rec.PendingRebootParameters = []string{"shared_buffers"}
|
||||
@@ -351,6 +353,7 @@ func TestRebootDBInstance_ClearsThePendingRebootParameters(t *testing.T) {
|
||||
// There is no standby to fail over to, so silently ignoring the flag would
|
||||
// report a failover that never happened.
|
||||
func TestRebootDBInstance_RejectsForceFailover(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
seedInstance(t, h.svc, availableRecord())
|
||||
|
||||
@@ -367,6 +370,7 @@ func TestRebootDBInstance_RejectsForceFailover(t *testing.T) {
|
||||
// Refusing to stop the VM over it would leave a customer unable to stop an
|
||||
// instance whose agent is wedged, so it is recorded and the stop continues.
|
||||
func TestStopDBInstance_RecordsAFailedGracefulStopAndContinues(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, true)
|
||||
seedInstance(t, h.svc, availableRecord())
|
||||
|
||||
@@ -391,6 +395,7 @@ func TestStopDBInstance_RecordsAFailedGracefulStopAndContinues(t *testing.T) {
|
||||
// way one that never held the VM does. Writing stopped on that alone would
|
||||
// report a VM still serving on the customer ENI as stopped.
|
||||
func TestStopDBInstance_FailsWhenNoNodeAnsweredButTheVMIsStillRunning(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
h.cmdr.stopNotOnNode = true
|
||||
h.vmState.state = instanceStateRunning
|
||||
@@ -408,6 +413,7 @@ func TestStopDBInstance_FailsWhenNoNodeAnsweredButTheVMIsStillRunning(t *testing
|
||||
// The same unanswered command is the normal shape of a VM that is genuinely
|
||||
// down, and that stop has to converge rather than fail.
|
||||
func TestStopDBInstance_CompletesWhenTheFleetConfirmsTheVMIsDown(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
h.cmdr.stopNotOnNode = true
|
||||
h.vmState.state = "stopped"
|
||||
@@ -424,6 +430,7 @@ func TestStopDBInstance_CompletesWhenTheFleetConfirmsTheVMIsDown(t *testing.T) {
|
||||
// drain and detach the data volume, so the first reading of the fleet has the VM
|
||||
// stopping. That is not down: the stop waits it out rather than returning on it.
|
||||
func TestStopDBInstance_WaitsForTheFleetToReportTheVMDown(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
h.vmState.detachReads = 1
|
||||
seedInstance(t, h.svc, availableRecord())
|
||||
@@ -439,6 +446,7 @@ func TestStopDBInstance_WaitsForTheFleetToReportTheVMDown(t *testing.T) {
|
||||
// The wait is bounded, and a VM the node accepted the stop for but never took
|
||||
// down has to end as a failure rather than as a stop that never returns.
|
||||
func TestStopDBInstance_FailsWhenTheVMNeverStops(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
h.cmdr.vm = nil
|
||||
seedInstance(t, h.svc, availableRecord())
|
||||
@@ -456,6 +464,7 @@ func TestStopDBInstance_FailsWhenTheVMNeverStops(t *testing.T) {
|
||||
// The reconciler resumes a stop with no caller watching, so it needs the same
|
||||
// confirmation before it calls an unanswered command a completed stop.
|
||||
func TestReconciler_DoesNotCallAStillRunningVMStopped(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
h.cmdr.stopNotOnNode = true
|
||||
h.vmState.state = instanceStateRunning
|
||||
@@ -474,6 +483,7 @@ func TestReconciler_DoesNotCallAStillRunningVMStopped(t *testing.T) {
|
||||
// The data volume, the customer ENI and the DNS record are all retained,
|
||||
// so a start comes back on the same datadir at the same address.
|
||||
func TestStopDBInstance_RetainsTheEndpointAndTheVolume(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
seed := availableRecord()
|
||||
seed.FormatAuthorized = true
|
||||
@@ -495,6 +505,7 @@ func TestStopDBInstance_RetainsTheEndpointAndTheVolume(t *testing.T) {
|
||||
// A pre-stop snapshot is a later phase's. Accepting it here would report a
|
||||
// snapshot the customer would then not find.
|
||||
func TestStopDBInstance_RejectsASnapshotIdentifier(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
seedInstance(t, h.svc, availableRecord())
|
||||
|
||||
@@ -508,6 +519,7 @@ func TestStopDBInstance_RejectsASnapshotIdentifier(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStartDBInstance_StartsTheVMAndReportsStarting(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
rec := availableRecord()
|
||||
rec.Status = StatusStopped
|
||||
@@ -528,6 +540,7 @@ func TestStartDBInstance_StartsTheVMAndReportsStarting(t *testing.T) {
|
||||
// A node restart drops the VM from memory, so the start relaunches it from the
|
||||
// persisted stopped-instance record rather than failing.
|
||||
func TestStartDBInstance_FallsBackWhenNoNodeHoldsTheVM(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
h.cmdr.notOnNode = true
|
||||
rec := availableRecord()
|
||||
@@ -584,6 +597,7 @@ func TestLifecycleOps_RejectAnIllegalStartingState(t *testing.T) {
|
||||
// A failed transition must not be left looking like one still in progress: the
|
||||
// customer would wait on a reboot that is never coming.
|
||||
func TestRebootDBInstance_MarksFailedWhenTheVMCommandFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
h.cmdr.err = errors.New("no node answered")
|
||||
seedInstance(t, h.svc, availableRecord())
|
||||
@@ -641,6 +655,7 @@ func TestReconciler_CompletesARestartOnAHeartbeatFromTheRestartedEngine(t *testi
|
||||
// is at most a persist floor behind the engine. Judging it by the raw stale
|
||||
// window left a database that came back cleanly rebooting until it timed out.
|
||||
func TestReconciler_CompletesARestartOnAPersistedHeartbeatInsideTheFloor(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
// Older than the stale window, younger than the window plus the floor, and
|
||||
// still after the transition it proves finished.
|
||||
@@ -656,6 +671,7 @@ func TestReconciler_CompletesARestartOnAPersistedHeartbeatInsideTheFloor(t *test
|
||||
// transition began would otherwise report the reboot as finished the instant it
|
||||
// started.
|
||||
func TestReconciler_IgnoresAHeartbeatPredatingTheRestart(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
now := time.Now().UTC()
|
||||
seedInstance(t, h.svc, restartingRecord(StatusRebooting, now, now.Add(-time.Minute)))
|
||||
@@ -668,6 +684,7 @@ func TestReconciler_IgnoresAHeartbeatPredatingTheRestart(t *testing.T) {
|
||||
// A restart that never comes back has to end somewhere: the customer sees a
|
||||
// broken instance either way, and failed is the state they can act on.
|
||||
func TestReconciler_MarksFailedWhenARestartOverrunsItsBound(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newLifecycleHarness(t, false)
|
||||
now := time.Now().UTC()
|
||||
started := now.Add(-2 * transitionTimeout)
|
||||
@@ -684,6 +701,7 @@ func TestReconciler_MarksFailedWhenARestartOverrunsItsBound(t *testing.T) {
|
||||
// customer a write-ahead log will bring back an Aria or MyISAM table would be a
|
||||
// false assurance about exactly the data most at risk.
|
||||
func TestUncleanStopMessage_IsEngineAware(t *testing.T) {
|
||||
t.Parallel()
|
||||
postgres := uncleanStopMessage(t.Context(), "postgres", "stopping the instance")
|
||||
assert.Contains(t, postgres, "could not be shut down cleanly before stopping the instance")
|
||||
assert.Contains(t, postgres, "write-ahead log")
|
||||
|
||||
@@ -24,6 +24,7 @@ func heldLease(remaining time.Duration) *ModifyLease {
|
||||
// the sweep runs a second grow — and for a class change, a second
|
||||
// replaceInstanceVM against the same data volume and endpoint ENI.
|
||||
func TestModifyDBInstance_HoldsTheLeaseSoAReconcilePassCannotReEnterTheChange(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
|
||||
@@ -50,6 +51,7 @@ func TestModifyDBInstance_HoldsTheLeaseSoAReconcilePassCannotReEnterTheChange(t
|
||||
// A pass that cannot take the lease leaves the instance alone entirely: the
|
||||
// holder is working on it, and the record already says what it is becoming.
|
||||
func TestReconciler_LeavesAModifyItsHolderIsStillInside(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifyingRecord(&PendingModifiedValues{AllocatedStorage: aws.Int64(50), RequestedAt: time.Now().UTC()})
|
||||
rec.ModifyLease = heldLease(modifyLeaseTTL)
|
||||
@@ -68,6 +70,7 @@ func TestReconciler_LeavesAModifyItsHolderIsStillInside(t *testing.T) {
|
||||
// the change over — which is what makes a leader that died mid-modify
|
||||
// recoverable rather than terminal.
|
||||
func TestReconciler_ResumesAModifyWhoseHolderIsGone(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifyingRecord(&PendingModifiedValues{AllocatedStorage: aws.Int64(50), RequestedAt: time.Now().UTC()})
|
||||
rec.ModifyLease = heldLease(-time.Second)
|
||||
@@ -107,6 +110,7 @@ func TestApplyPendingModifications_ReleasesTheLeaseWhateverTheOutcome(t *testing
|
||||
// modifying forever, since a pass that cannot take the lease does nothing. Past
|
||||
// the budget it is failed anyway, which is the state a retry is legal from.
|
||||
func TestReconciler_FailsAModifyWhoseHolderOverrunsTheBudget(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifyingRecord(&PendingModifiedValues{AllocatedStorage: aws.Int64(50), RequestedAt: time.Now().UTC()})
|
||||
rec.ModifyLease = heldLease(modifyLeaseTTL)
|
||||
@@ -125,6 +129,7 @@ func TestReconciler_FailsAModifyWhoseHolderOverrunsTheBudget(t *testing.T) {
|
||||
// A long-running apply has to keep its lease current, remain exclusive, and
|
||||
// stop as soon as another holder takes ownership.
|
||||
func TestWithModifyLease_RenewsAndCancelsTheApplyWhenTakenOver(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.svc.deps.ModifyLeaseTTL = 500 * time.Millisecond
|
||||
h.svc.deps.ModifyLeaseRefresh = 20 * time.Millisecond
|
||||
@@ -173,6 +178,7 @@ func TestWithModifyLease_RenewsAndCancelsTheApplyWhenTakenOver(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestModifyDBInstance_LeaseTakeoverLeavesTheTransitionRetryable(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.svc.deps.ModifyLeaseTTL = 500 * time.Millisecond
|
||||
h.svc.deps.ModifyLeaseRefresh = 10 * time.Millisecond
|
||||
@@ -199,6 +205,7 @@ func TestModifyDBInstance_LeaseTakeoverLeavesTheTransitionRetryable(t *testing.T
|
||||
}
|
||||
|
||||
func TestWithModifyLease_CancelsWhenRenewalsFailUntilExpiry(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.svc.deps.ModifyLeaseTTL = 100 * time.Millisecond
|
||||
h.svc.deps.ModifyLeaseRefresh = 10 * time.Millisecond
|
||||
@@ -230,6 +237,7 @@ func TestWithModifyLease_CancelsWhenRenewalsFailUntilExpiry(t *testing.T) {
|
||||
// Re-taking a lease we already hold has to be allowed, or a caller that claims
|
||||
// twice deadlocks against itself.
|
||||
func TestClaimModifyLease_RefusesAnotherHolderButNotItsOwn(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
kv := h.kv(t)
|
||||
|
||||
@@ -130,6 +130,7 @@ func createLargeMemoryParameterGroup(t *testing.T, h *modifyHarness) {
|
||||
// None of these interrupts service, so AWS applies them as soon as possible and
|
||||
// so does this — ApplyImmediately is not what gates them.
|
||||
func TestModifyDBInstance_AppliesTheNonDisruptiveSettingsAtOnce(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
|
||||
@@ -175,6 +176,7 @@ func TestModifyDBInstance_AppliesTheNonDisruptiveSettingsAtOnce(t *testing.T) {
|
||||
// Only the fact of the rotation is recorded. A password that reached KV
|
||||
// would be readable by anything that can read the bucket, forever.
|
||||
func TestModifyDBInstance_NeverPersistsTheRotatedPassword(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifiableRecord()
|
||||
rec.Bootstrap.MasterUserPassword = ""
|
||||
@@ -193,6 +195,7 @@ func TestModifyDBInstance_NeverPersistsTheRotatedPassword(t *testing.T) {
|
||||
// reported as applied but never reached the engine locks the customer out of
|
||||
// their own database with no signal.
|
||||
func TestModifyDBInstance_FailsWhenThePasswordCannotBeApplied(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarnessWithAgent(t, true)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
|
||||
@@ -212,6 +215,7 @@ func TestModifyDBInstance_FailsWhenThePasswordCannotBeApplied(t *testing.T) {
|
||||
// Changing a database's ingress is a live ENI re-association: no replace, no
|
||||
// new address, so the endpoint clients resolve does not move.
|
||||
func TestModifyDBInstance_ReassociatesTheSecurityGroupsOnTheEndpointENI(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
|
||||
@@ -231,6 +235,7 @@ func TestModifyDBInstance_ReassociatesTheSecurityGroupsOnTheEndpointENI(t *testi
|
||||
// An ENI cannot carry a group from another VPC, so the launch-time rule holds
|
||||
// at modify too — and rejecting it here keeps the failure ahead of every write.
|
||||
func TestModifyDBInstance_RejectsASecurityGroupFromAnotherVPC(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
|
||||
@@ -247,6 +252,7 @@ func TestModifyDBInstance_RejectsASecurityGroupFromAnotherVPC(t *testing.T) {
|
||||
// Re-sending the groups already attached is what Terraform does on every apply.
|
||||
// It is not a change, so it must not reach the ENI at all.
|
||||
func TestModifyDBInstance_IgnoresTheSecurityGroupsAlreadyAttached(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
|
||||
@@ -262,6 +268,7 @@ func TestModifyDBInstance_IgnoresTheSecurityGroupsAlreadyAttached(t *testing.T)
|
||||
// instance moves anywhere — a rejected request cannot cost a running database
|
||||
// its availability.
|
||||
func TestModifyDBInstance_RejectsAStorageShrinkBeforeAnythingMoves(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifiableRecord()
|
||||
rec.AllocatedStorage = 100
|
||||
@@ -286,6 +293,7 @@ func TestModifyDBInstance_RejectsAStorageShrinkBeforeAnythingMoves(t *testing.T)
|
||||
// current one accompanies unrelated changes constantly. Failing it would fail
|
||||
// every one of them; it contributes nothing to the resulting configuration instead.
|
||||
func TestModifyDBInstance_TreatsTheCurrentStorageSizeAsNoChange(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
|
||||
@@ -309,6 +317,7 @@ func TestModifyDBInstance_TreatsTheCurrentStorageSizeAsNoChange(t *testing.T) {
|
||||
// A request that changes nothing at all is answered with the instance as it
|
||||
// stands rather than an outage or an error.
|
||||
func TestModifyDBInstance_IsANoOpWhenNothingDiffers(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
|
||||
@@ -326,6 +335,7 @@ func TestModifyDBInstance_IsANoOpWhenNothingDiffers(t *testing.T) {
|
||||
// The db.* classes are a facade over the platform's instance types, so a
|
||||
// class with nothing behind it is rejected with the set that has.
|
||||
func TestModifyDBInstance_RejectsAnUnmappedInstanceClass(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
|
||||
@@ -342,6 +352,7 @@ func TestModifyDBInstance_RejectsAnUnmappedInstanceClass(t *testing.T) {
|
||||
// Until real groups are materialised the implicit default is the only name
|
||||
// that resolves, so any other one names a group that does not exist.
|
||||
func TestModifyDBInstance_RejectsAnUnknownParameterGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
|
||||
@@ -355,6 +366,7 @@ func TestModifyDBInstance_RejectsAnUnknownParameterGroup(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestModifyDBInstance_RejectsAnIncompatibleParameterGroupBeforeMutation(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
createLargeMemoryParameterGroup(t, h)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
@@ -376,6 +388,7 @@ func TestModifyDBInstance_RejectsAnIncompatibleParameterGroupBeforeMutation(t *t
|
||||
}
|
||||
|
||||
func TestModifyDBInstance_ValidatesClassChangeAgainstCurrentGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
createLargeMemoryParameterGroup(t, h)
|
||||
rec := modifiableRecord()
|
||||
@@ -398,6 +411,7 @@ func TestModifyDBInstance_ValidatesClassChangeAgainstCurrentGroup(t *testing.T)
|
||||
}
|
||||
|
||||
func TestModifyDBInstance_ValidatesSimultaneousGroupAndClassAgainstTargets(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
createLargeMemoryParameterGroup(t, h)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
@@ -417,6 +431,7 @@ func TestModifyDBInstance_ValidatesSimultaneousGroupAndClassAgainstTargets(t *te
|
||||
// These fields are stored rather than acted on, but a value outside AWS's
|
||||
// range would fail in a maintenance window nobody is watching.
|
||||
func TestModifyDBInstance_RejectsAnOutOfRangeBackupRetention(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
|
||||
@@ -472,6 +487,7 @@ func TestModifyDBInstance_RejectsTheUnimplementedParameters(t *testing.T) {
|
||||
// gp3 is the only type offered, so naming it is not a change and must not be
|
||||
// caught by the rejection that guards the types that are not offered.
|
||||
func TestModifyDBInstance_AcceptsTheStorageTypeItAlreadyHas(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
|
||||
@@ -487,6 +503,7 @@ func TestModifyDBInstance_AcceptsTheStorageTypeItAlreadyHas(t *testing.T) {
|
||||
// A disruptive change without ApplyImmediately is recorded and left for the
|
||||
// maintenance window: the database keeps serving until then.
|
||||
func TestModifyDBInstance_DefersADisruptiveChangeToTheMaintenanceWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
|
||||
@@ -523,6 +540,7 @@ func TestModifyDBInstance_DefersADisruptiveChangeToTheMaintenanceWindow(t *testi
|
||||
// A disruptive change needs a live engine to stop cleanly and a live agent to
|
||||
// apply parameters, so it is legal only from a state that has both.
|
||||
func TestModifyDBInstance_RejectsADisruptiveChangeWhileNotAvailable(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifiableRecord()
|
||||
rec.Status = StatusStopped
|
||||
@@ -542,6 +560,7 @@ func TestModifyDBInstance_RejectsADisruptiveChangeWhileNotAvailable(t *testing.T
|
||||
// A failed instance is exactly the one a customer retries a change on, so the
|
||||
// same change is accepted from there.
|
||||
func TestModifyDBInstance_AcceptsARetryFromFailed(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifiableRecord()
|
||||
rec.Status = StatusFailed
|
||||
@@ -561,6 +580,7 @@ func TestModifyDBInstance_AcceptsARetryFromFailed(t *testing.T) {
|
||||
// the volume grows, the VM comes back, and the instance stays in modifying with
|
||||
// the in-guest filesystem grow still outstanding.
|
||||
func TestModifyDBInstance_AppliesAStorageGrowImmediately(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seed := modifiableRecord()
|
||||
seed.FormatAuthorized = true
|
||||
@@ -594,6 +614,7 @@ func TestModifyDBInstance_AppliesAStorageGrowImmediately(t *testing.T) {
|
||||
// and the request still recorded, so the reconciler or the customer can retry
|
||||
// it rather than having to reconstruct what was asked for.
|
||||
func TestModifyDBInstance_FailsTheInstanceAndKeepsThePendingValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seedInstance(t, h.svc, modifiableRecord())
|
||||
h.storage.modifyErr = errors.New("the volume store rejected the resize")
|
||||
@@ -618,6 +639,7 @@ func TestModifyDBInstance_FailsTheInstanceAndKeepsThePendingValues(t *testing.T)
|
||||
}
|
||||
|
||||
func TestModifyDBInstance_RequiresAnIdentifier(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
|
||||
_, err := h.svc.ModifyDBInstance(t.Context(), &rds.ModifyDBInstanceInput{}, testAccountID)
|
||||
@@ -629,6 +651,7 @@ func TestModifyDBInstance_RequiresAnIdentifier(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestModifyDBInstance_RejectsAnUnknownInstance(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
|
||||
_, err := h.svc.ModifyDBInstance(t.Context(), modifyInput(), testAccountID)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
// InvalidParameterValue and every create without an explicit subnet group was
|
||||
// refused before it reached the launch path.
|
||||
func TestDefaultVPCID_SendsFilterNamesTheEC2SurfaceAccepts(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
|
||||
_, err := h.svc.CreateDBInstance(t.Context(), validCreateInput(), testAccountID)
|
||||
@@ -31,6 +32,7 @@ func TestDefaultVPCID_SendsFilterNamesTheEC2SurfaceAccepts(t *testing.T) {
|
||||
// An explicit subnet group carries its own VPC, so the default lookup — and the
|
||||
// filter that goes with it — must not be issued at all.
|
||||
func TestResolvePlacement_SkipsTheDefaultLookupForAnExplicitSubnetGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
_, err := h.svc.CreateDBSubnetGroup(t.Context(), subnetGroupInput(testSubnetGroup, "subnet-alpha"), testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -73,6 +73,7 @@ func TestParameterCatalog_EntriesAreWellFormed(t *testing.T) {
|
||||
// The size-derived defaults have to actually move with the class, or the
|
||||
// formulas are decoration and one end of the supported range is mis-tuned.
|
||||
func TestParameterCatalog_SizeDerivedDefaultsScaleWithClassMemory(t *testing.T) {
|
||||
t.Parallel()
|
||||
smallest, err := classMemoryMiB("db.t3.micro")
|
||||
require.NoError(t, err)
|
||||
largest, err := classMemoryMiB("db.m5.xlarge")
|
||||
@@ -95,6 +96,7 @@ func TestParameterCatalog_SizeDerivedDefaultsScaleWithClassMemory(t *testing.T)
|
||||
// shared_buffers is a quarter of class memory on real RDS, and it is the setting
|
||||
// whose being wrong at the small end stops the engine from starting.
|
||||
func TestParameterCatalog_SharedBuffersIsAQuarterOfClassMemory(t *testing.T) {
|
||||
t.Parallel()
|
||||
memoryMiB, err := classMemoryMiB("db.m5.large")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -169,6 +171,7 @@ func TestValidateParameterValue_AcceptsPostgresStringValues(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateParameterValue_AcceptsEveryPostgresSpellingOfABoolean(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, value := range []string{"on", "OFF", "true", "False", "yes", "no", "1", "0"} {
|
||||
_, err := enginePostgres.validateParameterValue("autovacuum", value)
|
||||
assert.NoError(t, err, "autovacuum rejected %q, which the engine accepts", value)
|
||||
@@ -327,6 +330,7 @@ func TestParameterCatalog_EachEngineExposesItsTLSEnforcementParameter(t *testing
|
||||
// A group's overrides are laid over the whole catalog, not merged into a subset:
|
||||
// every parameter is present, and one the group does not set carries its default.
|
||||
func TestResolveEffectiveParameters_OverlaysOverridesOnDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
resolved, err := enginePostgres.ResolveEffectiveParameters("db.t3.medium", map[string]string{"work_mem": "16384"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resolved, len(enginePostgres.CatalogParameterNames()))
|
||||
@@ -346,6 +350,7 @@ func TestResolveEffectiveParameters_OverlaysOverridesOnDefaults(t *testing.T) {
|
||||
// Sorted output is what lets a re-resolve that changed nothing produce a
|
||||
// byte-identical include, so an apply does not restart an engine for no reason.
|
||||
func TestResolveEffectiveParameters_IsSortedAndCarriesNoFormula(t *testing.T) {
|
||||
t.Parallel()
|
||||
resolved, err := enginePostgres.ResolveEffectiveParameters("db.m5.xlarge", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -361,6 +366,7 @@ func TestResolveEffectiveParameters_IsSortedAndCarriesNoFormula(t *testing.T) {
|
||||
// A stored value the catalog would now reject must not keep being handed to the
|
||||
// engine, so the resolve re-validates rather than trusting what was written.
|
||||
func TestResolveEffectiveParameters_RevalidatesStoredOverrides(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := enginePostgres.ResolveEffectiveParameters("db.t3.medium", map[string]string{"max_connections": "999999"})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "max_connections")
|
||||
@@ -427,6 +433,7 @@ func TestResolveEffectiveParameters_RejectsFatalCombinations(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveEffectiveParameters_AcceptsMinimalWALWithoutReplication(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := enginePostgres.ResolveEffectiveParameters("db.t3.micro", map[string]string{
|
||||
"wal_level": "minimal", "max_wal_senders": "0", "max_replication_slots": "0",
|
||||
})
|
||||
@@ -434,6 +441,7 @@ func TestResolveEffectiveParameters_AcceptsMinimalWALWithoutReplication(t *testi
|
||||
}
|
||||
|
||||
func TestParameterCatalog_Postgres18ApplyTypesAndBuiltCompressionMethods(t *testing.T) {
|
||||
t.Parallel()
|
||||
autovacuumWorkers, _ := enginePostgres.LookupParameter("autovacuum_max_workers")
|
||||
assert.Equal(t, ApplyTypeDynamic, autovacuumWorkers.ApplyType)
|
||||
|
||||
@@ -442,6 +450,7 @@ func TestParameterCatalog_Postgres18ApplyTypesAndBuiltCompressionMethods(t *test
|
||||
}
|
||||
|
||||
func TestResolveEffectiveParameters_RejectsAnUnknownClass(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := enginePostgres.ResolveEffectiveParameters("db.x99.mega", nil)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, awserrors.ErrorInvalidParameterValue, awserrors.ValidErrorCodeFromError(err),
|
||||
@@ -451,6 +460,7 @@ func TestResolveEffectiveParameters_RejectsAnUnknownClass(t *testing.T) {
|
||||
// AllowedValues is what tells a customer what an integer means, so a unitful
|
||||
// parameter has to report its unit rather than a bare pair of numbers.
|
||||
func TestParameterSpec_AllowedValuesDescribesTheConstraint(t *testing.T) {
|
||||
t.Parallel()
|
||||
shared, _ := enginePostgres.LookupParameter("shared_buffers")
|
||||
assert.Equal(t, "16-4194304 (8kB)", shared.AllowedValues())
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ func describedParameters(t *testing.T, h *createHarness, group string) map[strin
|
||||
}
|
||||
|
||||
func TestCreateDBParameterGroup_StoresAnEmptyGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
out, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
@@ -91,6 +92,7 @@ func TestCreateDBParameterGroup_StoresAnEmptyGroup(t *testing.T) {
|
||||
// An omitted family can only mean the one family this platform offers, so it
|
||||
// takes the pin rather than failing a Terraform config that leaves it out.
|
||||
func TestCreateDBParameterGroup_DefaultsTheFamilyAndRejectsAnother(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
input := parameterGroupInput(testParameterGroup)
|
||||
@@ -110,6 +112,7 @@ func TestCreateDBParameterGroup_DefaultsTheFamilyAndRejectsAnother(t *testing.T)
|
||||
// A customer group under the reserved prefix would be indistinguishable from the
|
||||
// implicit one, and would then be modifiable through a name that must not be.
|
||||
func TestCreateDBParameterGroup_RejectsTheReservedPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput("default.postgres18"), testAccountID)
|
||||
@@ -120,6 +123,7 @@ func TestCreateDBParameterGroup_RejectsTheReservedPrefix(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateDBParameterGroup_RejectsADuplicateName(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -133,6 +137,7 @@ func TestCreateDBParameterGroup_RejectsADuplicateName(t *testing.T) {
|
||||
// The default group is what CreateDBInstance resolves when a request names none,
|
||||
// so it has to be listable and readable without anyone having created it.
|
||||
func TestDescribeDBParameterGroups_ReportsTheImplicitDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
named, err := h.svc.DescribeDBParameterGroups(t.Context(),
|
||||
@@ -160,6 +165,7 @@ func parameterGroupNames(out *rds.DescribeDBParameterGroupsOutput) []string {
|
||||
// Synthesised rather than stored, so it must appear exactly once alongside the
|
||||
// customer's own groups rather than twice or not at all.
|
||||
func TestDescribeDBParameterGroups_ListsCustomerGroupsBesideTheDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput("zeta"), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -172,6 +178,7 @@ func TestDescribeDBParameterGroups_ListsCustomerGroupsBesideTheDefault(t *testin
|
||||
}
|
||||
|
||||
func TestDescribeDBParameterGroups_RejectsAnUnknownName(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
_, err := h.svc.DescribeDBParameterGroups(t.Context(),
|
||||
@@ -190,6 +197,7 @@ func TestDescribeDBParameterGroups_RejectsAnUnknownName(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestModifyDBParameterGroup_StoresValidatedOverrides(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -213,6 +221,7 @@ func TestModifyDBParameterGroup_StoresValidatedOverrides(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestModifyDBParameterGroup_PropagatesDynamicParametersToEveryAttachedInstance(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -254,6 +263,7 @@ func TestModifyDBParameterGroup_PropagatesDynamicParametersToEveryAttachedInstan
|
||||
// leaves the group holding a value the engine never adopted. That has to reach
|
||||
// the instance's apply status rather than being reported as in-sync.
|
||||
func TestModifyDBParameterGroup_RecordsAFailedApplyOnTheInstance(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarnessWithAgent(t, true)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -278,6 +288,7 @@ func TestModifyDBParameterGroup_RecordsAFailedApplyOnTheInstance(t *testing.T) {
|
||||
// A later apply the engine accepts is what clears it, so the instance does not
|
||||
// keep reporting a failure it has recovered from.
|
||||
func TestModifyDBParameterGroup_ASuccessfulApplyClearsTheRecordedFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -302,6 +313,7 @@ func TestModifyDBParameterGroup_ASuccessfulApplyClearsTheRecordedFailure(t *test
|
||||
// Propagation marks the instances that refused the set, not the group: an
|
||||
// instance that took the same values stays in-sync.
|
||||
func TestModifyDBParameterGroup_MarksOnlyTheInstancesWhoseApplyFailed(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarnessWithAgent(t, true)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -332,6 +344,7 @@ func TestModifyDBParameterGroup_MarksOnlyTheInstancesWhoseApplyFailed(t *testing
|
||||
}
|
||||
|
||||
func TestModifyDBParameterGroup_RecordsStaticParametersPendingReboot(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.agent.replyWith("max_connections")
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
@@ -354,6 +367,7 @@ func TestModifyDBParameterGroup_RecordsStaticParametersPendingReboot(t *testing.
|
||||
}
|
||||
|
||||
func TestModifyDBParameterGroup_DoesNotPropagateToAPendingAttachment(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -370,6 +384,7 @@ func TestModifyDBParameterGroup_DoesNotPropagateToAPendingAttachment(t *testing.
|
||||
}
|
||||
|
||||
func TestModifyDBParameterGroup_ReturnsAPropagationFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarnessWithAgent(t, true)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -395,6 +410,7 @@ func TestModifyDBParameterGroup_ReturnsAPropagationFailure(t *testing.T) {
|
||||
// A batch with one bad value must leave the group exactly as it was: a
|
||||
// half-applied modify would be a configuration nobody asked for.
|
||||
func TestModifyDBParameterGroup_WritesNothingWhenOneValueIsBad(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -449,6 +465,7 @@ func TestModifyDBParameterGroup_RejectsBadRequests(t *testing.T) {
|
||||
// AWS's own rule, and the one that keeps the default group a stable reference:
|
||||
// an instance that names it must get the catalog's values, not a tenant's edits.
|
||||
func TestModifyDBParameterGroup_RefusesTheDefaultGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
_, err := h.svc.ModifyDBParameterGroup(t.Context(),
|
||||
@@ -460,6 +477,7 @@ func TestModifyDBParameterGroup_RefusesTheDefaultGroup(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestModifyDBParameterGroup_RejectsAnUnknownGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
_, err := h.svc.ModifyDBParameterGroup(t.Context(),
|
||||
@@ -472,6 +490,7 @@ func TestModifyDBParameterGroup_RejectsAnUnknownGroup(t *testing.T) {
|
||||
// A computed default reaches the customer as the literal it evaluated to,
|
||||
// never as the formula that produced it.
|
||||
func TestDescribeDBParameters_ReportsComputedDefaultsAsLiterals(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
params := describedParameters(t, h, testDefaultPG)
|
||||
@@ -494,6 +513,7 @@ func TestDescribeDBParameters_ReportsComputedDefaultsAsLiterals(t *testing.T) {
|
||||
// a Terraform plan comparing what it wrote against what it reads would otherwise
|
||||
// show drift on every boolean, forever.
|
||||
func TestDescribeDBParameters_ReportsTheCustomersBooleanSpelling(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -516,6 +536,7 @@ func TestDescribeDBParameters_ReportsTheCustomersBooleanSpelling(t *testing.T) {
|
||||
// lower it. The refusal is the ordinary not-modifiable one, which names the
|
||||
// parameter rather than claiming the engine has no such setting.
|
||||
func TestDBParameterGroup_ReportsThePinnedTLSFloorAndRefusesToChangeIt(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -535,6 +556,7 @@ func TestDBParameterGroup_ReportsThePinnedTLSFloorAndRefusesToChangeIt(t *testin
|
||||
}
|
||||
|
||||
func TestDescribeDBParameters_DerivesApplyMethodForLegacyOverrides(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -553,6 +575,7 @@ func TestDescribeDBParameters_DerivesApplyMethodForLegacyOverrides(t *testing.T)
|
||||
}
|
||||
|
||||
func TestDescribeDBParameters_FiltersOnSource(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -577,6 +600,7 @@ func TestDescribeDBParameters_FiltersOnSource(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteDBParameterGroup_RemovesTheGroupAndItsValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -603,6 +627,7 @@ func TestDeleteDBParameterGroup_RemovesTheGroupAndItsValues(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteDBParameterGroup_RefusesTheDefaultGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
_, err := h.svc.DeleteDBParameterGroup(t.Context(),
|
||||
@@ -645,6 +670,7 @@ func TestDeleteDBParameterGroup_RefusesWhileAnInstanceReferencesIt(t *testing.T)
|
||||
// The group is the thing bootstrap consumes, so a create has to resolve it into
|
||||
// the literals the agent installs rather than leaving the set empty.
|
||||
func TestCreateDBInstance_ResolvesTheNamedGroupIntoTheBootstrapSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -676,6 +702,7 @@ func TestCreateDBInstance_ResolvesTheNamedGroupIntoTheBootstrapSet(t *testing.T)
|
||||
// An unnamed group takes the implicit default, which resolves to the catalog
|
||||
// alone rather than to nothing.
|
||||
func TestCreateDBInstance_ResolvesTheDefaultGroupWhenNoneIsNamed(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
_, err := h.svc.CreateDBInstance(t.Context(), validCreateInput(), testAccountID)
|
||||
@@ -687,6 +714,7 @@ func TestCreateDBInstance_ResolvesTheDefaultGroupWhenNoneIsNamed(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateDBInstance_RejectsAnUnknownParameterGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
input := validCreateInput()
|
||||
@@ -701,6 +729,7 @@ func TestCreateDBInstance_RejectsAnUnknownParameterGroup(t *testing.T) {
|
||||
// Both groups are taggable through the one ARN-addressed mutate path, so the
|
||||
// registry entries are what make an ARN a resource rather than a rejection.
|
||||
func TestTagActions_ReachBothGroupTypes(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBSubnetGroup(t.Context(), subnetGroupInput(testSubnetGroup, "subnet-alpha"), testAccountID)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -24,6 +24,7 @@ func modifyingRecord(pending *PendingModifiedValues) DBInstanceRecord {
|
||||
}
|
||||
|
||||
func TestApplyPendingModifications_StopsBeforeDestructiveWorkWhenCancelled(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifyingRecord(&PendingModifiedValues{AllocatedStorage: aws.Int64(50), RequestedAt: time.Now().UTC()})
|
||||
seedInstance(t, h.svc, rec)
|
||||
@@ -40,6 +41,7 @@ func TestApplyPendingModifications_StopsBeforeDestructiveWorkWhenCancelled(t *te
|
||||
// A grow with no class change alongside it keeps the VM: it goes down, the
|
||||
// volume grows, and the same VM comes back up on the same datadir.
|
||||
func TestApplyPendingModifications_GrowsStorageOnTheSameVM(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifyingRecord(&PendingModifiedValues{AllocatedStorage: aws.Int64(50), RequestedAt: time.Now().UTC()})
|
||||
seedInstance(t, h.svc, rec)
|
||||
@@ -63,6 +65,7 @@ func TestApplyPendingModifications_GrowsStorageOnTheSameVM(t *testing.T) {
|
||||
// A class change is a VM replace, and a grow asked for alongside it rides
|
||||
// the same outage rather than opening a second one.
|
||||
func TestApplyPendingModifications_ClassChangeReplacesTheVMAndCarriesTheGrow(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifyingRecord(&PendingModifiedValues{
|
||||
DBInstanceClass: "db.m5.large",
|
||||
@@ -91,6 +94,7 @@ func TestApplyPendingModifications_ClassChangeReplacesTheVMAndCarriesTheGrow(t *
|
||||
// still the one running, so the restart that follows is what adopts the
|
||||
// statically-scoped ones.
|
||||
func TestApplyPendingModifications_AppliesTheParametersBeforeTheOutage(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.agent.replyWith("shared_buffers")
|
||||
rec := modifyingRecord(&PendingModifiedValues{
|
||||
@@ -120,6 +124,7 @@ func TestApplyPendingModifications_AppliesTheParametersBeforeTheOutage(t *testin
|
||||
// stay pending until the customer reboots — the one path that still reports
|
||||
// pending-reboot.
|
||||
func TestApplyPendingModifications_KeepsThePendingRebootParametersWithoutAnOutage(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.agent.replyWith("shared_buffers")
|
||||
rec := modifyingRecord(&PendingModifiedValues{
|
||||
@@ -140,6 +145,7 @@ func TestApplyPendingModifications_KeepsThePendingRebootParametersWithoutAnOutag
|
||||
// the customer would reboot a healthy database to clear it, and Terraform would
|
||||
// see it in every configuration read.
|
||||
func TestApplyPendingModifications_ClassChangeClearsThePendingRebootParameters(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.agent.replyWith("shared_buffers")
|
||||
rec := modifyingRecord(&PendingModifiedValues{
|
||||
@@ -163,6 +169,7 @@ func TestApplyPendingModifications_ClassChangeClearsThePendingRebootParameters(t
|
||||
// customer asked for one change, and delivering half of it silently is the
|
||||
// failure mode this closes.
|
||||
func TestApplyPendingModifications_KeepsThePendingValuesWhenTheChangeFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.storage.modifyErr = errors.New("the volume store is unavailable")
|
||||
pending := &PendingModifiedValues{
|
||||
@@ -188,6 +195,7 @@ func TestApplyPendingModifications_KeepsThePendingValuesWhenTheChangeFails(t *te
|
||||
// Nothing outstanding is not an error: a record whose values have already
|
||||
// landed is drained again by any resumed pass.
|
||||
func TestApplyPendingModifications_IsANoOpWithNothingOutstanding(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifyingRecord(nil)
|
||||
seedInstance(t, h.svc, rec)
|
||||
@@ -200,6 +208,7 @@ func TestApplyPendingModifications_IsANoOpWithNothingOutstanding(t *testing.T) {
|
||||
// The last step of a grow: the control plane has already grown the volume, and
|
||||
// this is what turns it into capacity the database can use.
|
||||
func TestFinishFilesystemGrow_ExtendsTheGuestAndClearsThePending(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifyingRecord(&PendingModifiedValues{FilesystemGrowPending: true, RequestedAt: time.Now().UTC()})
|
||||
rec.AllocatedStorage = 50
|
||||
@@ -222,6 +231,7 @@ func TestFinishFilesystemGrow_ExtendsTheGuestAndClearsThePending(t *testing.T) {
|
||||
// volume is bigger and the database cannot use it, which is exactly the state
|
||||
// the reconciler has to keep retrying.
|
||||
func TestFinishFilesystemGrow_KeepsTheStepWhenTheGuestFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarnessWithAgent(t, true)
|
||||
rec := modifyingRecord(&PendingModifiedValues{FilesystemGrowPending: true, RequestedAt: time.Now().UTC()})
|
||||
seedInstance(t, h.svc, rec)
|
||||
@@ -236,6 +246,7 @@ func TestFinishFilesystemGrow_KeepsTheStepWhenTheGuestFails(t *testing.T) {
|
||||
// A leader that died mid-modify leaves the change recorded and nothing else, so
|
||||
// the next pass re-runs it rather than waiting on a VM that was never touched.
|
||||
func TestReconciler_ResumesAnInterruptedModify(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifyingRecord(&PendingModifiedValues{AllocatedStorage: aws.Int64(50), RequestedAt: time.Now().UTC()})
|
||||
seedInstance(t, h.svc, rec)
|
||||
@@ -253,6 +264,7 @@ func TestReconciler_ResumesAnInterruptedModify(t *testing.T) {
|
||||
// The in-guest grow can only run once the agent is back, so the reconciler is
|
||||
// what issues it — a customer's modify finishes without them watching.
|
||||
func TestReconciler_FinishesTheFilesystemGrowOnceTheAgentIsBack(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifyingRecord(&PendingModifiedValues{FilesystemGrowPending: true, RequestedAt: time.Now().UTC()})
|
||||
beat := time.Now().UTC()
|
||||
@@ -275,6 +287,7 @@ func TestReconciler_FinishesTheFilesystemGrowOnceTheAgentIsBack(t *testing.T) {
|
||||
// A modify whose VM never comes back cannot sit in modifying forever: the
|
||||
// instance is failed with the reason, which is a state a retry is legal from.
|
||||
func TestReconciler_FailsAModifyThatOverrunsItsBudget(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifyingRecord(nil)
|
||||
started := time.Now().UTC().Add(-2 * transitionTimeout)
|
||||
@@ -292,6 +305,7 @@ func TestReconciler_FailsAModifyThatOverrunsItsBudget(t *testing.T) {
|
||||
// A modify that cannot be applied at all is failed rather than retried
|
||||
// forever, and the request stays recorded so the retry has something to run.
|
||||
func TestReconciler_FailsAModifyItCannotApplyWithinTheBudget(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.storage.modifyErr = errors.New("the volume store is unavailable")
|
||||
rec := modifyingRecord(&PendingModifiedValues{AllocatedStorage: aws.Int64(50), RequestedAt: time.Now().UTC()})
|
||||
@@ -311,6 +325,7 @@ func TestReconciler_FailsAModifyItCannotApplyWithinTheBudget(t *testing.T) {
|
||||
// Inside the budget a failed attempt is retried on the next pass rather than
|
||||
// failing the instance on the first transient error.
|
||||
func TestReconciler_RetriesAFailedModifyInsideTheBudget(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.storage.modifyErr = errors.New("the volume store is briefly unavailable")
|
||||
rec := modifyingRecord(&PendingModifiedValues{AllocatedStorage: aws.Int64(50), RequestedAt: time.Now().UTC()})
|
||||
@@ -328,6 +343,7 @@ func TestReconciler_RetriesAFailedModifyInsideTheBudget(t *testing.T) {
|
||||
// rather than carry the old set forward — a shared_buffers computed for the old
|
||||
// class is wrong at the new one in whichever direction the class moved.
|
||||
func TestApplyPendingModifications_ReResolvesTheParametersForTheNewClass(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.agent.replyWith("shared_buffers")
|
||||
rec := modifyingRecord(&PendingModifiedValues{
|
||||
@@ -359,6 +375,7 @@ func TestApplyPendingModifications_ReResolvesTheParametersForTheNewClass(t *test
|
||||
// A re-resolve rather than a merge: a parameter the old group set and the new
|
||||
// one does not reverts to its catalog default instead of lingering.
|
||||
func TestApplyPendingModifications_ParameterGroupChangeRevertsTheOldGroupsValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.agent.replyWith("")
|
||||
|
||||
@@ -396,6 +413,7 @@ func TestApplyPendingModifications_ParameterGroupChangeRevertsTheOldGroupsValues
|
||||
// without it outranking the outstanding request the instance would report
|
||||
// applying on every pass while the engine runs the set it already had.
|
||||
func TestApplyPendingModifications_AFailedParameterApplyIsRecordedOnTheInstance(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarnessWithAgent(t, true)
|
||||
rec := modifyingRecord(&PendingModifiedValues{
|
||||
DBParameterGroupName: testDefaultGroup,
|
||||
|
||||
@@ -89,6 +89,7 @@ func (h *reconcileHarness) statusOf(t *testing.T, id string) (Status, string) {
|
||||
}
|
||||
|
||||
func TestReconciler_MarksAvailableOnHealthyHeartbeatFromARunningVM(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t)
|
||||
rec := healthyRecord()
|
||||
rec.FormatAuthorized = true
|
||||
@@ -119,6 +120,7 @@ func TestReconciler_MarksAvailableOnHealthyHeartbeatFromARunningVM(t *testing.T)
|
||||
// engine reported before the platform finished bringing the VM up, or the beat
|
||||
// is stale — either way the instance is not ready.
|
||||
func TestReconciler_HoldsCreatingWhenTheVMIsNotRunning(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t)
|
||||
h.state.state = "pending"
|
||||
seedInstance(t, h.svc, healthyRecord())
|
||||
@@ -169,6 +171,7 @@ func TestReconciler_HoldsCreatingUntilTheEngineIsHealthy(t *testing.T) {
|
||||
// floor; judging that beat by the raw stale window failed a live database at the
|
||||
// transition timeout.
|
||||
func TestReconciler_CompletesACreateOnAPersistedHeartbeatInsideTheFloor(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t)
|
||||
rec := healthyRecord()
|
||||
// Older than the stale window, younger than the window plus the floor: what a
|
||||
@@ -186,6 +189,7 @@ func TestReconciler_CompletesACreateOnAPersistedHeartbeatInsideTheFloor(t *testi
|
||||
// A create that never comes up has to end somewhere: the customer sees a broken
|
||||
// instance either way, and failed is the state they can act on.
|
||||
func TestReconciler_MarksFailedWhenTheBootstrapTimesOut(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t, func(d *Deps) { d.BootstrapTimeout = time.Minute })
|
||||
rec := healthyRecord()
|
||||
rec.Agent = AgentState{
|
||||
@@ -206,6 +210,7 @@ func TestReconciler_MarksFailedWhenTheBootstrapTimesOut(t *testing.T) {
|
||||
// Inside the window a slow bootstrap is still a bootstrap: a false failed is
|
||||
// worse than a slow one.
|
||||
func TestReconciler_LeavesASlowBootstrapAloneInsideTheWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t, func(d *Deps) { d.BootstrapTimeout = time.Hour })
|
||||
rec := healthyRecord()
|
||||
rec.Agent = AgentState{}
|
||||
@@ -222,6 +227,7 @@ func TestReconciler_LeavesASlowBootstrapAloneInsideTheWindow(t *testing.T) {
|
||||
// owner. available and failed are the classifier's, and covered in
|
||||
// recovery_test.go.
|
||||
func TestReconciler_LeavesSettledInstancesAlone(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t)
|
||||
rec := healthyRecord()
|
||||
rec.Status = StatusStopped
|
||||
@@ -238,6 +244,7 @@ func TestReconciler_LeavesSettledInstancesAlone(t *testing.T) {
|
||||
// A VM-state lookup that fails is not evidence the instance is unhealthy, so the
|
||||
// pass reports the error rather than silently holding — or worse, failing — it.
|
||||
func TestReconciler_SurfacesAVMStateLookupFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t)
|
||||
h.state.err = errors.New("no node answered the describe")
|
||||
seedInstance(t, h.svc, healthyRecord())
|
||||
@@ -253,6 +260,7 @@ func TestReconciler_SurfacesAVMStateLookupFailure(t *testing.T) {
|
||||
// Without a VM-state resolver the heartbeat alone decides, so a node with EC2
|
||||
// unwired still makes progress rather than stalling every create.
|
||||
func TestReconciler_FallsBackToTheHeartbeatWhenVMStateIsUnwired(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t, func(d *Deps) { d.InstanceState = nil })
|
||||
seedInstance(t, h.svc, healthyRecord())
|
||||
|
||||
@@ -266,6 +274,7 @@ func TestReconciler_FallsBackToTheHeartbeatWhenVMStateIsUnwired(t *testing.T) {
|
||||
// claimant must not also believe it is leader, or two nodes would transition
|
||||
// the same records.
|
||||
func TestReconciler_ElectsASingleLeader(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t)
|
||||
other := NewReconciler(h.svc, "node-b")
|
||||
|
||||
@@ -283,6 +292,7 @@ func TestReconciler_ElectsASingleLeader(t *testing.T) {
|
||||
|
||||
// A node that never won the lease must not delete the holder's key on shutdown.
|
||||
func TestReconciler_RelinquishOnlyReleasesItsOwnLease(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t)
|
||||
other := NewReconciler(h.svc, "node-b")
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ func (h *reconcileHarness) recordOf(t *testing.T, id string) DBInstanceRecord {
|
||||
// An instance that has been dark for a week must not keep reporting available:
|
||||
// the failure mode is silent for exactly as long as nobody tries to connect.
|
||||
func TestReconciler_MarksADarkInstanceFailed(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t, func(d *Deps) { d.FailureGrace = time.Nanosecond })
|
||||
h.state.state = "stopped"
|
||||
seedInstance(t, h.svc, darkRecord())
|
||||
@@ -108,6 +109,7 @@ func TestReconciler_MarksADarkInstanceFailed(t *testing.T) {
|
||||
// customer's monitoring can alert on is the whole point of detecting failure
|
||||
// when nothing repairs it.
|
||||
func TestReconciler_RecordsAnEventForADarkInstance(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t, func(d *Deps) { d.FailureGrace = time.Nanosecond })
|
||||
h.state.state = "stopped"
|
||||
seedInstance(t, h.svc, darkRecord())
|
||||
@@ -128,6 +130,7 @@ func TestReconciler_RecordsAnEventForADarkInstance(t *testing.T) {
|
||||
// fleet-wide fan-out, so issuing one per instance per pass would put the whole
|
||||
// fleet's liveness on the bus every 15 seconds.
|
||||
func TestReconciler_LeavesAHealthyInstanceUntouchedWithoutAVMLookup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t)
|
||||
seedInstance(t, h.svc, servingRecord())
|
||||
|
||||
@@ -181,6 +184,7 @@ func TestReconciler_HoldsAvailableOnHalfTheEvidence(t *testing.T) {
|
||||
// live database can be reported down, which is what a transient fleet describe
|
||||
// during a node restart cannot get past.
|
||||
func TestReconciler_StartsTheClockBeforeFailingWithinTheGrace(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t, func(d *Deps) { d.FailureGrace = time.Hour })
|
||||
h.state.state = "stopped"
|
||||
seedInstance(t, h.svc, darkRecord())
|
||||
@@ -196,6 +200,7 @@ func TestReconciler_StartsTheClockBeforeFailingWithinTheGrace(t *testing.T) {
|
||||
// The clock a previous leader stamped is what the grace is measured against, so
|
||||
// a leader change does not restart the window and defer detection indefinitely.
|
||||
func TestReconciler_FailsOnAClockStampedByAnEarlierLeader(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t, func(d *Deps) { d.FailureGrace = time.Minute })
|
||||
h.state.state = "stopped"
|
||||
rec := darkRecord()
|
||||
@@ -218,6 +223,7 @@ func TestReconciler_FailsOnAClockStampedByAnEarlierLeader(t *testing.T) {
|
||||
// matters more without a recovery ladder than with one, since nothing else is
|
||||
// watching.
|
||||
func TestReconciler_TheClockResetsOnlyOnAHealthyHeartbeat(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t, func(d *Deps) { d.FailureGrace = time.Hour })
|
||||
rec := darkRecord()
|
||||
stamped := time.Now().UTC().Add(-30 * time.Minute)
|
||||
@@ -250,6 +256,7 @@ func TestReconciler_TheClockResetsOnlyOnAHealthyHeartbeat(t *testing.T) {
|
||||
// allows failed → available, so an instance the AMI or EC2 brought back reports
|
||||
// itself recovered without an operator touching it.
|
||||
func TestReconciler_ClearsFailedOnARecoveredHeartbeat(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t)
|
||||
rec := servingRecord()
|
||||
rec.Status = StatusFailed
|
||||
@@ -278,6 +285,7 @@ func TestReconciler_ClearsFailedOnARecoveredHeartbeat(t *testing.T) {
|
||||
// Nothing retries a failed instance in v1.0, so a still-dark one is left where
|
||||
// it is rather than having its reason and clock rewritten every pass.
|
||||
func TestReconciler_LeavesAStillDarkFailedInstanceAlone(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t, func(d *Deps) { d.FailureGrace = time.Nanosecond })
|
||||
h.state.state = "stopped"
|
||||
rec := darkRecord()
|
||||
@@ -298,6 +306,7 @@ func TestReconciler_LeavesAStillDarkFailedInstanceAlone(t *testing.T) {
|
||||
// floor. Judging a persisted beat by the raw stale window would report a steady
|
||||
// fleet as dark for most of every persist cycle.
|
||||
func TestReconciler_AllowsThePersistFloorOnAPersistedHeartbeat(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t, func(d *Deps) { d.FailureGrace = time.Nanosecond })
|
||||
h.state.state = "stopped"
|
||||
rec := servingRecord()
|
||||
@@ -318,6 +327,7 @@ func TestReconciler_AllowsThePersistFloorOnAPersistedHeartbeat(t *testing.T) {
|
||||
// itself earns no slack, and a leader that took the last beat before the VM went
|
||||
// dark must call it failed on the stale window rather than on window plus floor.
|
||||
func TestReconciler_UsesTheTightBoundOnABeatThisNodeHandled(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t, func(d *Deps) { d.FailureGrace = time.Nanosecond })
|
||||
h.state.state = instanceStateStopped
|
||||
rec := servingRecord()
|
||||
@@ -339,6 +349,7 @@ func TestReconciler_UsesTheTightBoundOnABeatThisNodeHandled(t *testing.T) {
|
||||
// gathered at all, so nothing is ever reported failed. Detection goes missing
|
||||
// rather than being declared on the heartbeat alone.
|
||||
func TestReconciler_NeverFailsAnInstanceWhenVMStateIsUnwired(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t, func(d *Deps) {
|
||||
d.InstanceState = nil
|
||||
d.FailureGrace = time.Nanosecond
|
||||
@@ -355,6 +366,7 @@ func TestReconciler_NeverFailsAnInstanceWhenVMStateIsUnwired(t *testing.T) {
|
||||
// A VM lookup that fails is not evidence the instance is down, so the pass
|
||||
// reports the error rather than starting a failure clock on a guess.
|
||||
func TestReconciler_SurfacesAVMLookupFailureWithoutFailingTheInstance(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newReconcileHarness(t, func(d *Deps) { d.FailureGrace = time.Nanosecond })
|
||||
h.state.err = errors.New("no node answered the describe")
|
||||
seedInstance(t, h.svc, darkRecord())
|
||||
@@ -371,6 +383,7 @@ func TestReconciler_SurfacesAVMLookupFailureWithoutFailingTheInstance(t *testing
|
||||
// A failed instance has to be able to explain itself to the customer, not just
|
||||
// to the log.
|
||||
func TestProjectDBInstance_ReportsTheFailureReason(t *testing.T) {
|
||||
t.Parallel()
|
||||
svc := NewService(nil, testRegion)
|
||||
rec := defaultRecord()
|
||||
rec.Status = StatusFailed
|
||||
|
||||
@@ -30,6 +30,7 @@ func seedReplaceable(t *testing.T, h *modifyHarness, rec DBInstanceRecord) DBIns
|
||||
}
|
||||
|
||||
func TestReplaceInstanceVM_StopsBeforeDestructiveWorkWhenCancelled(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := seedReplaceable(t, h, modifiableRecord())
|
||||
ctx, cancel := context.WithCancelCause(t.Context())
|
||||
@@ -46,6 +47,7 @@ func TestReplaceInstanceVM_StopsBeforeDestructiveWorkWhenCancelled(t *testing.T)
|
||||
// both. Minting either would move the address clients resolve or hand the
|
||||
// customer an empty database.
|
||||
func TestReplaceInstanceVM_ReusesTheEndpointENIAndDataVolume(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := seedReplaceable(t, h, modifiableRecord())
|
||||
|
||||
@@ -74,6 +76,7 @@ func TestReplaceInstanceVM_ReusesTheEndpointENIAndDataVolume(t *testing.T) {
|
||||
// The engine is checkpointed and the old VM is gone before the new one comes
|
||||
// up, or two VMs hold the same datadir.
|
||||
func TestReplaceInstanceVM_IAMFailureLeavesTheCurrentVMServing(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := seedReplaceable(t, h, modifiableRecord())
|
||||
iamErr := errors.New("IAM store unavailable")
|
||||
@@ -95,6 +98,7 @@ func TestReplaceInstanceVM_IAMFailureLeavesTheCurrentVMServing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReplaceInstanceVM_StopsTheEngineAndTerminatesTheOldVMFirst(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := seedReplaceable(t, h, modifiableRecord())
|
||||
|
||||
@@ -118,6 +122,7 @@ func TestReplaceInstanceVM_StopsTheEngineAndTerminatesTheOldVMFirst(t *testing.T
|
||||
// While the old index entry exists the superseded agent's IMDS credentials
|
||||
// still resolve to this DB instance, so it goes before the new one lands.
|
||||
func TestReplaceInstanceVM_RewritesTheInstanceIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := seedReplaceable(t, h, modifiableRecord())
|
||||
|
||||
@@ -138,6 +143,7 @@ func TestReplaceInstanceVM_RewritesTheInstanceIndex(t *testing.T) {
|
||||
// The record names the new VM and forgets the old one's health, while every
|
||||
// field the endpoint is built from is left exactly as it was.
|
||||
func TestReplaceInstanceVM_RecordsTheNewVMAndKeepsTheEndpoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seed := modifiableRecord()
|
||||
seed.FormatAuthorized = true
|
||||
@@ -173,6 +179,7 @@ func TestReplaceInstanceVM_RecordsTheNewVMAndKeepsTheEndpoint(t *testing.T) {
|
||||
// A grow riding a class change takes the only window in which nothing holds the
|
||||
// volume: after the old VM is gone and before the new one exists.
|
||||
func TestReplaceInstanceVM_GrowsTheVolumeWhileNoVMHoldsIt(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := seedReplaceable(t, h, modifiableRecord())
|
||||
|
||||
@@ -195,6 +202,7 @@ func TestReplaceInstanceVM_GrowsTheVolumeWhileNoVMHoldsIt(t *testing.T) {
|
||||
// An unmapped class has no instance type behind it, so the replace is refused
|
||||
// before the VM it would land on is torn down.
|
||||
func TestReplaceInstanceVM_RefusesAnUnmappedRecordedClass(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
seed := modifiableRecord()
|
||||
seed.DBInstanceClass = "db.r5.24xlarge"
|
||||
@@ -230,6 +238,7 @@ func TestReplaceInstanceVM_RefusesWithoutThePersistedIdentity(t *testing.T) {
|
||||
// a replacement: a new ENI would come up on a different address than the one
|
||||
// DNS and the serving certificate name.
|
||||
func TestReplaceInstanceVM_RefusesToMintAReplacementEndpoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := seedReplaceable(t, h, modifiableRecord())
|
||||
h.launch.enis.describeMissing = true
|
||||
@@ -285,6 +294,7 @@ func TestReplaceInstanceVM_LeavesTheRecordRecoverableOnEveryFailure(t *testing.T
|
||||
// A failed launch tears down what it created rather than leaving the DB
|
||||
// instance's own ENI and volume attached to a VM nobody will ever record.
|
||||
func TestReplaceInstanceVM_UnwindsAFailedLaunchWithoutTouchingTheEndpoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := seedReplaceable(t, h, modifiableRecord())
|
||||
h.launch.launcher.err = errors.New("no capacity on any node")
|
||||
@@ -298,6 +308,7 @@ func TestReplaceInstanceVM_UnwindsAFailedLaunchWithoutTouchingTheEndpoint(t *tes
|
||||
// Auto-recovery replaces onto the sizing the record already carries, so an
|
||||
// empty class in the request is not a class change.
|
||||
func TestReplaceInstanceVM_KeepsTheRecordedSizingWhenNoneIsRequested(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := seedReplaceable(t, h, modifiableRecord())
|
||||
|
||||
@@ -312,6 +323,7 @@ func TestReplaceInstanceVM_KeepsTheRecordedSizingWhenNoneIsRequested(t *testing.
|
||||
// The customer's own event ring is where a replace becomes attributable to the
|
||||
// change or the failure that caused it.
|
||||
func TestReplaceInstanceVM_RecordsWhyTheVMWasReplaced(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := seedReplaceable(t, h, modifiableRecord())
|
||||
|
||||
@@ -327,6 +339,7 @@ func TestReplaceInstanceVM_RecordsWhyTheVMWasReplaced(t *testing.T) {
|
||||
// initialize: the datadir is already there, and a second initdb would be
|
||||
// destructive. The user data is what carries the identity it fetches with.
|
||||
func TestReplaceInstanceVM_LaunchesWithTheInstancesOwnIdentity(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := seedReplaceable(t, h, modifiableRecord())
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ func restoreInput() *rds.RestoreDBInstanceFromDBSnapshotInput {
|
||||
}
|
||||
|
||||
func TestRestoreDBInstanceFromDBSnapshot_BuildsANewInstanceOnTheSnapshotsData(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
snapshot := h.seedSnapshot(t)
|
||||
|
||||
@@ -75,6 +76,7 @@ func TestRestoreDBInstanceFromDBSnapshot_BuildsANewInstanceOnTheSnapshotsData(t
|
||||
}
|
||||
|
||||
func TestRestoreDBInstanceFromDBSnapshot_IAMFailurePrecedesReservationAndVolume(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshot(t)
|
||||
iamErr := errors.New("IAM store unavailable")
|
||||
@@ -93,6 +95,7 @@ func TestRestoreDBInstanceFromDBSnapshot_IAMFailurePrecedesReservationAndVolume(
|
||||
// Source-instance configuration comes from the snapshot when the restore
|
||||
// request does not override it; platform-owned settings use current defaults.
|
||||
func TestRestoreDBInstanceFromDBSnapshot_FallsBackToTheSnapshotsConfiguration(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshot(t)
|
||||
|
||||
@@ -111,6 +114,7 @@ func TestRestoreDBInstanceFromDBSnapshot_FallsBackToTheSnapshotsConfiguration(t
|
||||
}
|
||||
|
||||
func TestRestoreDBInstanceFromDBSnapshot_DefaultsRetentionAndTakesAnAutomatedBackup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
now := time.Now().UTC()
|
||||
h.svc.deps.Backup = BackupPolicy{
|
||||
@@ -154,6 +158,7 @@ func TestRestoreDBInstanceFromDBSnapshot_DefaultsRetentionAndTakesAnAutomatedBac
|
||||
}
|
||||
|
||||
func TestRestoreDBInstanceFromDBSnapshot_HonoursTheRequestedOverrides(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshot(t)
|
||||
|
||||
@@ -180,6 +185,7 @@ func TestRestoreDBInstanceFromDBSnapshot_HonoursTheRequestedOverrides(t *testing
|
||||
// CreateVolume refuses a size below the snapshot's, and a shrink has nowhere to
|
||||
// put the data the snapshot already holds.
|
||||
func TestRestoreDBInstanceFromDBSnapshot_RejectsStorageBelowTheSnapshots(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshot(t)
|
||||
|
||||
@@ -196,6 +202,7 @@ func TestRestoreDBInstanceFromDBSnapshot_RejectsStorageBelowTheSnapshots(t *test
|
||||
// The volume holds its source snapshot undeletable, so a restore that never
|
||||
// produced an instance must not leave one behind.
|
||||
func TestRestoreDBInstanceFromDBSnapshot_UnwindsTheVolumeAndTheRecordWhenTheLaunchFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshot(t)
|
||||
h.launch.launcher.err = errors.New("no node had capacity")
|
||||
@@ -212,6 +219,7 @@ func TestRestoreDBInstanceFromDBSnapshot_UnwindsTheVolumeAndTheRecordWhenTheLaun
|
||||
}
|
||||
|
||||
func TestRestoreDBInstanceFromDBSnapshot_IndexFailureWithdrawsTheRecordedLaunch(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshot(t)
|
||||
// This invalid KV key makes the instance-index write fail after recordLaunch
|
||||
@@ -228,6 +236,7 @@ func TestRestoreDBInstanceFromDBSnapshot_IndexFailureWithdrawsTheRecordedLaunch(
|
||||
}
|
||||
|
||||
func TestRestoreDBInstanceFromDBSnapshot_RecordLaunchDoesNotOverwriteAConcurrentReplacement(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshot(t)
|
||||
var replacement DBInstanceRecord
|
||||
@@ -242,6 +251,7 @@ func TestRestoreDBInstanceFromDBSnapshot_RecordLaunchDoesNotOverwriteAConcurrent
|
||||
}
|
||||
|
||||
func TestRestoreDBInstanceFromDBSnapshot_RollbackDoesNotDeleteAConcurrentReplacement(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshot(t)
|
||||
h.launch.launcher.instanceID = "invalid instance id"
|
||||
@@ -259,6 +269,7 @@ func TestRestoreDBInstanceFromDBSnapshot_RollbackDoesNotDeleteAConcurrentReplace
|
||||
}
|
||||
|
||||
func TestRestoreDBInstanceFromDBSnapshot_RejectsATakenIdentifier(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshot(t)
|
||||
|
||||
@@ -272,6 +283,7 @@ func TestRestoreDBInstanceFromDBSnapshot_RejectsATakenIdentifier(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRestoreDBInstanceFromDBSnapshot_RejectsASnapshotThatDoesNotExist(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
|
||||
_, err := h.svc.RestoreDBInstanceFromDBSnapshot(t.Context(), restoreInput(), testAccountID)
|
||||
@@ -281,6 +293,7 @@ func TestRestoreDBInstanceFromDBSnapshot_RejectsASnapshotThatDoesNotExist(t *tes
|
||||
|
||||
// A snapshot still being taken has no data to restore from.
|
||||
func TestRestoreDBInstanceFromDBSnapshot_RejectsASnapshotStillBeingTaken(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
kv, err := h.svc.bucket(t.Context(), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -332,6 +345,7 @@ func TestRestoreDBInstanceFromDBSnapshot_RejectsUnimplementedParameters(t *testi
|
||||
// The datadir is written in one engine's on-disk format and no other can read
|
||||
// it, so a request naming a different engine is refused rather than honoured.
|
||||
func TestRestoreDBInstanceFromDBSnapshot_RejectsAnEngineChange(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshot(t)
|
||||
|
||||
@@ -346,6 +360,7 @@ func TestRestoreDBInstanceFromDBSnapshot_RejectsAnEngineChange(t *testing.T) {
|
||||
// Read from the volume rather than echoed from the snapshot: a cluster whose
|
||||
// storage key has gone would otherwise report encryption it is not giving.
|
||||
func TestRestoreDBInstanceFromDBSnapshot_RefusesAnUnencryptedVolumeForAnEncryptedSnapshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshot(t)
|
||||
h.launch.volumes.encrypted = false
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestInstanceTypeForClass_KnownClasses(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]string{
|
||||
"db.t3.micro": "t3.micro",
|
||||
"db.t3.small": "t3.small",
|
||||
@@ -26,6 +27,7 @@ func TestInstanceTypeForClass_KnownClasses(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInstanceTypeForClass_UnmappedClassRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Real AWS classes the platform does not offer, the bare EC2 type, and a
|
||||
// class-shaped string that is not one — all must be rejected rather than
|
||||
// guessed at by stripping the db. prefix.
|
||||
@@ -43,6 +45,7 @@ func TestInstanceTypeForClass_UnmappedClassRejected(t *testing.T) {
|
||||
// name an instance type that table actually defines. A typo here would surface
|
||||
// as a launch failure after the data volume and ENI already exist.
|
||||
func TestDBInstanceClasses_ResolveInSizingTable(t *testing.T) {
|
||||
t.Parallel()
|
||||
for class, instanceType := range dbInstanceClasses {
|
||||
vcpus, ok := instancetypes.DefaultVCPUs(instanceType)
|
||||
assert.True(t, ok, "class %q maps to unknown instance type %q", class, instanceType)
|
||||
@@ -51,6 +54,7 @@ func TestDBInstanceClasses_ResolveInSizingTable(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSupportedInstanceClasses_SortedAndComplete(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, []string{
|
||||
"db.m5.large", "db.m5.xlarge",
|
||||
"db.t3.large", "db.t3.medium", "db.t3.micro", "db.t3.small",
|
||||
@@ -62,6 +66,7 @@ func TestSupportedInstanceClasses_SortedAndComplete(t *testing.T) {
|
||||
// memory, so reading the head of the sorted list would advertise a shared_buffers
|
||||
// no small instance ever runs.
|
||||
func TestSmallestInstanceClass_IsTheLeastMemory(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "db.t3.micro", SmallestInstanceClass())
|
||||
|
||||
least, err := classMemoryMiB(SmallestInstanceClass())
|
||||
|
||||
@@ -130,6 +130,7 @@ func snapshotInput() *rds.CreateDBSnapshotInput {
|
||||
}
|
||||
|
||||
func TestCreateDBSnapshot_QuiescesTheEngineAndRecordsTheSnapshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshotSource(t)
|
||||
|
||||
@@ -194,6 +195,7 @@ func TestCreateDBSnapshot_ReturnsTheInstanceToWhereItWasFound(t *testing.T) {
|
||||
// A stopped instance's datadir was sealed by the graceful stop, which is the
|
||||
// checkpoint a quiesce would be forcing — so there is no engine to hold.
|
||||
func TestCreateDBSnapshot_DoesNotQuiesceAStoppedInstance(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := availableRecord()
|
||||
rec.Status = StatusStopped
|
||||
@@ -210,6 +212,7 @@ func TestCreateDBSnapshot_DoesNotQuiesceAStoppedInstance(t *testing.T) {
|
||||
// A snapshot that could not be quiesced is still a restorable backup, so it
|
||||
// is taken and reported honestly rather than refused.
|
||||
func TestCreateDBSnapshot_FallsBackToCrashConsistentWhenTheQuiesceFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, true)
|
||||
h.seedSnapshotSource(t)
|
||||
|
||||
@@ -232,6 +235,7 @@ func TestCreateDBSnapshot_FallsBackToCrashConsistentWhenTheQuiesceFails(t *testi
|
||||
// customer a write-ahead log will bring back an Aria or MyISAM table would be a
|
||||
// false assurance about exactly the data most at risk.
|
||||
func TestCrashConsistentSnapshotMessage_IsEngineAware(t *testing.T) {
|
||||
t.Parallel()
|
||||
postgres := crashConsistentSnapshotMessage(t.Context(), "postgres")
|
||||
assert.Contains(t, postgres, "crash consistent")
|
||||
assert.Contains(t, postgres, "write-ahead log")
|
||||
@@ -251,6 +255,7 @@ func TestCrashConsistentSnapshotMessage_IsEngineAware(t *testing.T) {
|
||||
// The record only ever described a snapshot that now does not exist, so it goes
|
||||
// with it rather than holding the identifier for a reconciler to puzzle over.
|
||||
func TestCreateDBSnapshot_WithdrawsTheRecordWhenTheSnapshotFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshotSource(t)
|
||||
h.snaps.createErr = awserrors.Errorf(awserrors.ErrorInternalError, "the volume store refused the snapshot")
|
||||
@@ -264,6 +269,7 @@ func TestCreateDBSnapshot_WithdrawsTheRecordWhenTheSnapshotFails(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateDBSnapshot_RejectsATakenIdentifier(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshotSource(t)
|
||||
_, err := h.svc.CreateDBSnapshot(t.Context(), snapshotInput(), testAccountID)
|
||||
@@ -279,6 +285,7 @@ func TestCreateDBSnapshot_RejectsATakenIdentifier(t *testing.T) {
|
||||
// One snapshot per instance at a time: the CAS into backing-up is the guard, so
|
||||
// an instance already there is refused rather than entering the same window.
|
||||
func TestCreateDBSnapshot_RefusesAnInstanceAlreadyBackingUp(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := availableRecord()
|
||||
rec.Status = StatusBackingUp
|
||||
@@ -296,6 +303,7 @@ func TestCreateDBSnapshot_RefusesAnInstanceAlreadyBackingUp(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateDBSnapshot_RejectsAnUnusableIdentifier(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshotSource(t)
|
||||
|
||||
@@ -311,6 +319,7 @@ func TestCreateDBSnapshot_RejectsAnUnusableIdentifier(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDescribeDBSnapshots_FiltersByInstanceAndType(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshotSource(t)
|
||||
_, err := h.svc.CreateDBSnapshot(t.Context(), snapshotInput(), testAccountID)
|
||||
@@ -348,6 +357,7 @@ func TestDescribeDBSnapshots_FiltersByInstanceAndType(t *testing.T) {
|
||||
|
||||
// A client polling a create would otherwise read "gone" for "not ready".
|
||||
func TestDescribeDBSnapshots_NamedSnapshotThatDoesNotExistIsAnError(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
|
||||
_, err := h.svc.DescribeDBSnapshots(t.Context(), &rds.DescribeDBSnapshotsInput{
|
||||
@@ -377,6 +387,7 @@ func TestDescribeDBSnapshots_RejectsUnhonouredScoping(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteDBSnapshot_RemovesTheSnapshotAndItsRecord(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshotSource(t)
|
||||
_, err := h.svc.CreateDBSnapshot(t.Context(), snapshotInput(), testAccountID)
|
||||
@@ -402,6 +413,7 @@ func TestDeleteDBSnapshot_RemovesTheSnapshotAndItsRecord(t *testing.T) {
|
||||
// A COW snapshot references its source volume's chunks, so a deleted instance's
|
||||
// volume is retained until the last snapshot holding it goes.
|
||||
func TestDeleteDBSnapshot_ReleasesARetainedVolumeWhenItWasTheLastHolder(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshotSource(t)
|
||||
_, err := h.svc.CreateDBSnapshot(t.Context(), snapshotInput(), testAccountID)
|
||||
@@ -433,6 +445,7 @@ func TestDeleteDBSnapshot_ReleasesARetainedVolumeWhenItWasTheLastHolder(t *testi
|
||||
// record was written with: another snapshot taken since would otherwise read as
|
||||
// "nothing holds it".
|
||||
func TestDeleteDBSnapshot_KeepsARetainedVolumeAnotherSnapshotStillHolds(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshotSource(t)
|
||||
_, err := h.svc.CreateDBSnapshot(t.Context(), snapshotInput(), testAccountID)
|
||||
@@ -467,6 +480,7 @@ func TestDeleteDBSnapshot_KeepsARetainedVolumeAnotherSnapshotStillHolds(t *testi
|
||||
// The raw EC2 code says nothing a customer can act on, so the fault names the
|
||||
// restored instance they have to remove first.
|
||||
func TestDeleteDBSnapshot_NamesTheRestoredInstanceStillReadingFromIt(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshotSource(t)
|
||||
_, err := h.svc.CreateDBSnapshot(t.Context(), snapshotInput(), testAccountID)
|
||||
@@ -492,6 +506,7 @@ func TestDeleteDBSnapshot_NamesTheRestoredInstanceStillReadingFromIt(t *testing.
|
||||
// A snapshot still being taken has no data to remove and an in-flight writer
|
||||
// that would recreate the record.
|
||||
func TestDeleteDBSnapshot_RefusesASnapshotStillBeingTaken(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
kv, err := h.svc.bucket(t.Context(), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -513,6 +528,7 @@ func TestDeleteDBSnapshot_RefusesASnapshotStillBeingTaken(t *testing.T) {
|
||||
// A snapshot still being taken has no PercentProgress, because reporting full
|
||||
// progress would have a client restore from something that does not exist.
|
||||
func TestProjectDBSnapshot_ReportsProgressOnlyWhenTheDataExists(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := DBSnapshotRecord{
|
||||
DBSnapshotIdentifier: testSnapshotID,
|
||||
@@ -528,6 +544,7 @@ func TestProjectDBSnapshot_ReportsProgressOnlyWhenTheDataExists(t *testing.T) {
|
||||
// The EC2 snapshot is tagged with the DB snapshot identifier before the record
|
||||
// is flipped, so its presence is what says the data exists.
|
||||
func TestReconciler_AdoptsTheEC2SnapshotOfAnUnfinishedDBSnapshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := NewReconciler(h.svc, "node-a")
|
||||
|
||||
@@ -560,6 +577,7 @@ func TestReconciler_AdoptsTheEC2SnapshotOfAnUnfinishedDBSnapshot(t *testing.T) {
|
||||
// A record left in creating would hold its name forever while naming nothing a
|
||||
// customer can restore.
|
||||
func TestReconciler_DoesNotAdoptAnotherAccountsSnapshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
reconciler := NewReconciler(h.svc, "node-a")
|
||||
|
||||
@@ -593,6 +611,7 @@ func TestReconciler_DoesNotAdoptAnotherAccountsSnapshot(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReconciler_KeepsCreatingSnapshotWhenEC2LookupIsIncomplete(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.snaps.describeErr = errors.New("snapshot metadata temporarily unavailable")
|
||||
reconciler := NewReconciler(h.svc, "node-a")
|
||||
@@ -617,6 +636,7 @@ func TestReconciler_KeepsCreatingSnapshotWhenEC2LookupIsIncomplete(t *testing.T)
|
||||
}
|
||||
|
||||
func TestReconciler_DoesNotDeleteAConcurrentlyCompletedSnapshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
reconciler := NewReconciler(h.svc, "node-a")
|
||||
|
||||
@@ -649,6 +669,7 @@ func TestReconciler_DoesNotDeleteAConcurrentlyCompletedSnapshot(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReconciler_RejectsAmbiguousEC2Snapshots(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
reconciler := NewReconciler(h.svc, "node-a")
|
||||
|
||||
@@ -683,6 +704,7 @@ func TestReconciler_RejectsAmbiguousEC2Snapshots(t *testing.T) {
|
||||
// A completed authoritative lookup is the only evidence that no EC2 snapshot
|
||||
// was cut. Once it succeeds with no match, the stale name can be released.
|
||||
func TestReconciler_WithdrawsADBSnapshotWhoseDataWasNeverCut(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := NewReconciler(h.svc, "node-a")
|
||||
|
||||
@@ -707,6 +729,7 @@ func TestReconciler_WithdrawsADBSnapshotWhoseDataWasNeverCut(t *testing.T) {
|
||||
// Inside the window the record still belongs to a live worker, so touching it
|
||||
// would race the write that is about to land.
|
||||
func TestReconciler_LeavesAFreshCreatingSnapshotAlone(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
rec := NewReconciler(h.svc, "node-a")
|
||||
|
||||
@@ -758,6 +781,7 @@ func TestReconciler_ReturnsAnInstanceStuckInBackingUp(t *testing.T) {
|
||||
// Inside the bound the snapshot is still running, and cutting it short would
|
||||
// leave the engine quiesced with nobody to release it.
|
||||
func TestReconciler_LeavesAnInFlightSnapshotAlone(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
reconciler := NewReconciler(h.svc, "node-a")
|
||||
|
||||
@@ -782,6 +806,7 @@ func TestReconciler_LeavesAnInFlightSnapshotAlone(t *testing.T) {
|
||||
// The Terraform provider reads tags from the describe as well as from
|
||||
// ListTagsForResource, so the two have to agree.
|
||||
func TestDBSnapshotTags_ReadBackThroughBothPaths(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshotSource(t)
|
||||
|
||||
@@ -811,6 +836,7 @@ func TestDBSnapshotTags_ReadBackThroughBothPaths(t *testing.T) {
|
||||
// The record has to survive a round trip through KV, because that is the only
|
||||
// copy a restore reads once the instance it came from is gone.
|
||||
func TestDBSnapshotRecord_SurvivesARoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
updated := time.Now().UTC().Truncate(time.Second)
|
||||
rec := DBSnapshotRecord{
|
||||
DBSnapshotIdentifier: testSnapshotID,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func TestStatus_Valid(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, s := range []Status{
|
||||
StatusCreating, StatusAvailable, StatusModifying, StatusBackingUp,
|
||||
StatusRebooting, StatusStopping, StatusStarting, StatusStopped,
|
||||
@@ -22,6 +23,7 @@ func TestStatus_Valid(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCanTransition_Lifecycle(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
from, to Status
|
||||
want bool
|
||||
@@ -81,6 +83,7 @@ func TestCanTransition_Lifecycle(t *testing.T) {
|
||||
// Every state the machine can reach must itself be a state the machine knows,
|
||||
// or the reconciler could write a status it then refuses to read back.
|
||||
func TestTransitions_TargetsAreValidStates(t *testing.T) {
|
||||
t.Parallel()
|
||||
for from, targets := range transitions {
|
||||
assert.True(t, from.Valid(), "source status %q should be valid", from)
|
||||
for _, to := range targets {
|
||||
|
||||
@@ -100,6 +100,7 @@ func TestValidateStorageGrow(t *testing.T) {
|
||||
// The volume store is grow-only, so a resumed grow that re-issued the modify
|
||||
// would be rejected as a shrink. It reads the volume first instead.
|
||||
func TestGrowDataVolume_SkipsAVolumeAlreadyAtTheTarget(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.storage.sizes[testDataVolume] = 50
|
||||
|
||||
@@ -110,6 +111,7 @@ func TestGrowDataVolume_SkipsAVolumeAlreadyAtTheTarget(t *testing.T) {
|
||||
// A volume the store cannot describe must not read as zero: zero is smaller
|
||||
// than every target, so the grow would go ahead against a volume that is gone.
|
||||
func TestGrowDataVolume_FailsWhenTheVolumeIsGone(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.storage.missing = true
|
||||
|
||||
@@ -120,6 +122,7 @@ func TestGrowDataVolume_FailsWhenTheVolumeIsGone(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGrowDataVolume_FailsWithoutAVolume(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
|
||||
require.Error(t, h.svc.growDataVolume(t.Context(), "", 50))
|
||||
@@ -128,6 +131,7 @@ func TestGrowDataVolume_FailsWithoutAVolume(t *testing.T) {
|
||||
// ModifyVolume refuses a volume a running VM holds, so the order is the
|
||||
// whole mechanism — engine down, VM down, grow, VM back.
|
||||
func TestGrowInstanceStorage_StopsTheEngineAndTheVMBeforeGrowing(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
rec := modifiableRecord()
|
||||
|
||||
@@ -151,6 +155,7 @@ func TestGrowInstanceStorage_StopsTheEngineAndTheVMBeforeGrowing(t *testing.T) {
|
||||
// store refuses a resize until it has. The grow holds off until the fleet
|
||||
// reports the VM down rather than resizing on the accepted command.
|
||||
func TestGrowInstanceStorage_WaitsForTheDetachBeforeGrowing(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.vmState.detachReads = 1
|
||||
rec := modifiableRecord()
|
||||
@@ -168,6 +173,7 @@ func TestGrowInstanceStorage_WaitsForTheDetachBeforeGrowing(t *testing.T) {
|
||||
// A wedged agent must not block a grow: the engine stop is a courtesy, and the
|
||||
// VM stop that follows is what actually releases the volume.
|
||||
func TestGrowInstanceStorage_ProceedsWhenTheEngineWillNotStop(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarnessWithAgent(t, true)
|
||||
rec := modifiableRecord()
|
||||
|
||||
@@ -178,6 +184,7 @@ func TestGrowInstanceStorage_ProceedsWhenTheEngineWillNotStop(t *testing.T) {
|
||||
// A rejected grow must not turn a failed modification into an outage. The VM
|
||||
// restarts on its unchanged volume before the original error is returned.
|
||||
func TestGrowInstanceStorage_RestartsTheVMWhenTheGrowFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.storage.modifyErr = errors.New("the volume store is unavailable")
|
||||
rec := modifiableRecord()
|
||||
@@ -191,6 +198,7 @@ func TestGrowInstanceStorage_RestartsTheVMWhenTheGrowFails(t *testing.T) {
|
||||
// Both failures matter when the grow and its recovery fail: the first explains
|
||||
// the unchanged storage and the second explains the continuing outage.
|
||||
func TestGrowInstanceStorage_ReportsTheGrowAndRestartFailures(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.storage.modifyErr = errors.New("the volume store is unavailable")
|
||||
h.storage.onModify = func() { h.cmdr.err = errors.New("the node did not answer") }
|
||||
@@ -207,6 +215,7 @@ func TestGrowInstanceStorage_ReportsTheGrowAndRestartFailures(t *testing.T) {
|
||||
// Recovery belongs to the new lease holder after a takeover. The stale holder
|
||||
// must not restart the VM while its replacement may be retrying the grow.
|
||||
func TestGrowInstanceStorage_DoesNotRestartAfterLosingTheModifyLease(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
ctx, cancel := context.WithCancelCause(t.Context())
|
||||
h.storage.modifyErr = errors.New("the volume store is unavailable")
|
||||
@@ -223,6 +232,7 @@ func TestGrowInstanceStorage_DoesNotRestartAfterLosingTheModifyLease(t *testing.
|
||||
// happen must not be followed by a resize that would be rejected — or worse,
|
||||
// accepted against a volume a live guest is writing to.
|
||||
func TestGrowInstanceStorage_FailsWhenTheVMWillNotStop(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.cmdr.err = errors.New("the node did not answer")
|
||||
rec := modifiableRecord()
|
||||
|
||||
@@ -10,10 +10,12 @@ import (
|
||||
)
|
||||
|
||||
func TestAccountBucketName(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "rds-account-123456789012", AccountBucketName(testAccountID))
|
||||
}
|
||||
|
||||
func TestKeyPaths(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "db-instances/orders-db", DBInstanceKey("orders-db"))
|
||||
assert.Equal(t, "db-snapshots/orders-db-final", DBSnapshotKey("orders-db-final"))
|
||||
assert.Equal(t, "db-subnet-groups/prod-db-subnets", DBSubnetGroupKey("prod-db-subnets"))
|
||||
@@ -29,6 +31,7 @@ func TestKeyPaths(t *testing.T) {
|
||||
// Each key must sit under its own prefix, so an enumeration of one resource type
|
||||
// can never pick up another's records.
|
||||
func TestKeyPathsSitUnderTheirPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.True(t, strings.HasPrefix(DBInstanceKey("x"), DBInstancesPrefix()))
|
||||
assert.True(t, strings.HasPrefix(DBSnapshotKey("x"), DBSnapshotsPrefix()))
|
||||
assert.True(t, strings.HasPrefix(DBSubnetGroupKey("x"), DBSubnetGroupsPrefix()))
|
||||
@@ -44,11 +47,13 @@ func TestKeyPathsSitUnderTheirPrefix(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewStore_NilConn(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := NewStore(nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNewStore_Valid(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
s, err := NewStore(nc)
|
||||
require.NoError(t, err)
|
||||
@@ -56,6 +61,7 @@ func TestNewStore_Valid(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetOrCreateAccountBucket_Idempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
js := testutil.NewJetStream(t, nc)
|
||||
|
||||
@@ -72,6 +78,7 @@ func TestGetOrCreateAccountBucket_Idempotent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetOrCreateSystemBucket_Idempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, nc, _ := testutil.StartTestJetStream(t)
|
||||
js := testutil.NewJetStream(t, nc)
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ func subnetGroupInput(name string, subnetIDs ...string) *rds.CreateDBSubnetGroup
|
||||
// Terraform module against a platform that exposes one zone, and rejecting it
|
||||
// buys no safety.
|
||||
func TestCreateDBSubnetGroup_AcceptsASingleSubnet(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
out, err := h.svc.CreateDBSubnetGroup(t.Context(), subnetGroupInput(testSubnetGroup, "subnet-alpha"), testAccountID)
|
||||
@@ -47,6 +48,7 @@ func TestCreateDBSubnetGroup_AcceptsASingleSubnet(t *testing.T) {
|
||||
// Stored verbatim: the group keeps every subnet the customer supplied, so the
|
||||
// placement logic can change later without a record migration.
|
||||
func TestCreateDBSubnetGroup_StoresEverySubnetSupplied(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
_, err := h.svc.CreateDBSubnetGroup(t.Context(),
|
||||
@@ -65,6 +67,7 @@ func TestCreateDBSubnetGroup_StoresEverySubnetSupplied(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateDBSubnetGroup_RejectsAMissingSubnet(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
_, err := h.svc.CreateDBSubnetGroup(t.Context(),
|
||||
@@ -78,6 +81,7 @@ func TestCreateDBSubnetGroup_RejectsAMissingSubnet(t *testing.T) {
|
||||
// The check that actually matters: a group spanning two VPCs cannot host an
|
||||
// instance at all, because the endpoint ENI lives in exactly one of them.
|
||||
func TestCreateDBSubnetGroup_RejectsACrossVPCSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
h.network.subnets = append(h.network.subnets, &ec2.Subnet{
|
||||
SubnetId: aws.String("subnet-other"),
|
||||
@@ -96,6 +100,7 @@ func TestCreateDBSubnetGroup_RejectsACrossVPCSet(t *testing.T) {
|
||||
// The describe is issued as the caller, so another account's subnet does not
|
||||
// come back at all — it is reported as invalid rather than leaked or accepted.
|
||||
func TestCreateDBSubnetGroup_RejectsACrossAccountSubnet(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
_, err := h.svc.CreateDBSubnetGroup(t.Context(),
|
||||
@@ -143,6 +148,7 @@ func TestCreateDBSubnetGroup_RejectsMalformedRequests(t *testing.T) {
|
||||
// The name is the reservation, so a repeat is a conflict rather than a silent
|
||||
// overwrite of the first group's subnets.
|
||||
func TestCreateDBSubnetGroup_RejectsADuplicateName(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBSubnetGroup(t.Context(), subnetGroupInput(testSubnetGroup, "subnet-alpha"), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -154,6 +160,7 @@ func TestCreateDBSubnetGroup_RejectsADuplicateName(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDescribeDBSubnetGroups_ListsAndNames(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBSubnetGroup(t.Context(), subnetGroupInput("zeta", "subnet-alpha"), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -176,6 +183,7 @@ func TestDescribeDBSubnetGroups_ListsAndNames(t *testing.T) {
|
||||
// A client polling a create would read an empty list as "gone" rather than
|
||||
// "not there", so a named group that does not exist is an error.
|
||||
func TestDescribeDBSubnetGroups_RejectsAnUnknownName(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
_, err := h.svc.DescribeDBSubnetGroups(t.Context(),
|
||||
@@ -186,6 +194,7 @@ func TestDescribeDBSubnetGroups_RejectsAnUnknownName(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteDBSubnetGroup_RemovesAnUnusedGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBSubnetGroup(t.Context(), subnetGroupInput(testSubnetGroup, "subnet-alpha"), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -233,6 +242,7 @@ func TestDeleteDBSubnetGroup_RefusesWhileAnInstanceReferencesIt(t *testing.T) {
|
||||
// The group is what decides where the endpoint lands, so a create naming one has
|
||||
// to place the ENI in its subnet rather than in the account's default VPC.
|
||||
func TestCreateDBInstance_PlacesTheEndpointFromTheNamedSubnetGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBSubnetGroup(t.Context(), subnetGroupInput(testSubnetGroup, "subnet-zebra"), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -253,6 +263,7 @@ func TestCreateDBInstance_PlacesTheEndpointFromTheNamedSubnetGroup(t *testing.T)
|
||||
// was placed from is only useful to a client if it survives the projection, and
|
||||
// the Terraform provider reads db_subnet_group_name from exactly here.
|
||||
func TestDescribeDBInstances_ReportsTheSubnetGroupTheInstanceWasPlacedFrom(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
_, err := h.svc.CreateDBSubnetGroup(t.Context(), subnetGroupInput(testSubnetGroup, "subnet-zebra"), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -281,6 +292,7 @@ func TestDescribeDBInstances_ReportsTheSubnetGroupTheInstanceWasPlacedFrom(t *te
|
||||
// Placing the endpoint somewhere else would put it in a subnet nobody chose, so
|
||||
// a group that does not exist fails the create rather than falling back.
|
||||
func TestCreateDBInstance_RejectsAnUnknownSubnetGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
|
||||
input := validCreateInput()
|
||||
|
||||
@@ -38,6 +38,7 @@ func listTags(t *testing.T, h *createHarness, id string) map[string]string {
|
||||
}
|
||||
|
||||
func TestCreateDBInstance_TagsRoundTripThroughBothReadPaths(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
input := validCreateInput()
|
||||
input.Tags = awsTags("env", "prod", "team", "platform")
|
||||
@@ -61,6 +62,7 @@ func TestCreateDBInstance_TagsRoundTripThroughBothReadPaths(t *testing.T) {
|
||||
// The same rules apply at create as at AddTagsToResource, and they run before
|
||||
// the identifier is reserved so a rejected create leaves nothing behind.
|
||||
func TestCreateDBInstance_InvalidTagsLeaveNoRecord(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
input := validCreateInput()
|
||||
input.Tags = awsTags("aws:cloudformation:stack-name", "mine")
|
||||
@@ -73,6 +75,7 @@ func TestCreateDBInstance_InvalidTagsLeaveNoRecord(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListTagsForResource_UntaggedInstanceIsAnEmptyList(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
seedCreated(t, h, testDBInstanceID)
|
||||
|
||||
@@ -82,6 +85,7 @@ func TestListTagsForResource_UntaggedInstanceIsAnEmptyList(t *testing.T) {
|
||||
// AddTagsToResource is a merge, and a repeated key overwrites rather than
|
||||
// duplicates, so a re-run of the same apply converges.
|
||||
func TestAddTagsToResource_MergesAndOverwrites(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
seedCreated(t, h, testDBInstanceID)
|
||||
arn := aws.String(DBInstanceARN(testRegion, testAccountID, testDBInstanceID))
|
||||
@@ -102,6 +106,7 @@ func TestAddTagsToResource_MergesAndOverwrites(t *testing.T) {
|
||||
// A key repeated inside one request keeps its last value rather than failing,
|
||||
// which is what AWS does.
|
||||
func TestAddTagsToResource_DuplicateKeyInOneRequestKeepsTheLastValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
seedCreated(t, h, testDBInstanceID)
|
||||
|
||||
@@ -115,6 +120,7 @@ func TestAddTagsToResource_DuplicateKeyInOneRequestKeepsTheLastValue(t *testing.
|
||||
}
|
||||
|
||||
func TestRemoveTagsFromResource_RemovesNamedKeysAndIgnoresAbsentOnes(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
seedCreated(t, h, testDBInstanceID)
|
||||
arn := aws.String(DBInstanceARN(testRegion, testAccountID, testDBInstanceID))
|
||||
@@ -171,6 +177,7 @@ func TestTagActions_RejectInvalidTags(t *testing.T) {
|
||||
// The limit is a per-resource one, so two individually legal requests that
|
||||
// together exceed it are rejected on the second.
|
||||
func TestAddTagsToResource_LimitAppliesToTheMergedResult(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
seedCreated(t, h, testDBInstanceID)
|
||||
arn := aws.String(DBInstanceARN(testRegion, testAccountID, testDBInstanceID))
|
||||
@@ -200,6 +207,7 @@ func TestAddTagsToResource_LimitAppliesToTheMergedResult(t *testing.T) {
|
||||
// A well-formed ARN naming nothing is the resource's own fault, not an ARN
|
||||
// error: the two failures carry different messages on purpose.
|
||||
func TestTagActions_MissingResourceIsTheResourcesOwnFault(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
arn := aws.String(DBInstanceARN(testRegion, testAccountID, "no-such-db"))
|
||||
|
||||
@@ -223,6 +231,7 @@ func TestTagActions_MissingResourceIsTheResourcesOwnFault(t *testing.T) {
|
||||
// Answering with an empty list would be indistinguishable from an untagged
|
||||
// snapshot and would let an apply appear to tag something that does not exist.
|
||||
func TestTagActions_MissingSnapshotIsRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
|
||||
_, err := h.svc.ListTagsForResource(t.Context(), &rds.ListTagsForResourceInput{
|
||||
@@ -233,6 +242,7 @@ func TestTagActions_MissingSnapshotIsRejected(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListTagsForResource_AcceptsAnAutomatedSnapshotARN(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
id := AutomatedSnapshotIdentifier(testDBInstanceID, time.Date(2026, 7, 24, 3, 4, 0, 0, time.UTC))
|
||||
kv, err := h.svc.bucket(t.Context(), testAccountID)
|
||||
@@ -251,6 +261,7 @@ func TestListTagsForResource_AcceptsAnAutomatedSnapshotARN(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTagActions_ForeignAccountARNIsRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
seedCreated(t, h, testDBInstanceID)
|
||||
|
||||
@@ -264,6 +275,7 @@ func TestTagActions_ForeignAccountARNIsRejected(t *testing.T) {
|
||||
// Every tag write is a read-modify-write of the same record, so without CAS the
|
||||
// last writer would silently discard the others' keys.
|
||||
func TestTagWrites_ConcurrentAddsAndRemovesDoNotLoseEachOther(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, "")
|
||||
seedCreated(t, h, testDBInstanceID)
|
||||
arn := aws.String(DBInstanceARN(testRegion, testAccountID, testDBInstanceID))
|
||||
|
||||
@@ -48,6 +48,7 @@ func requireNoTLSToEnforce(t *testing.T, err error, parameter, group string) {
|
||||
// The three states loadCA distinguishes, because the gate reads the middle one
|
||||
// as a refusal and must not read the last one as one.
|
||||
func TestTLSAvailable_ReportsWhetherTheDeploymentCanServeTLS(t *testing.T) {
|
||||
t.Parallel()
|
||||
configured := NewService(nil, testRegion).WithDeps(Deps{LoadCA: newTestCA(t)})
|
||||
available, err := configured.tlsAvailable()
|
||||
require.NoError(t, err)
|
||||
@@ -101,6 +102,7 @@ func TestCreateDBInstance_EnforcesTLSWithNoParameterGroupOfItsOwn(t *testing.T)
|
||||
// Enforcement the deployment cannot serve would launch an instance no client can
|
||||
// reach, so the binding is refused where the customer can see it.
|
||||
func TestCreateDBInstance_RefusesEnforcementWithNoClusterCA(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newCreateHarness(t, testBaseDomain)
|
||||
withoutClusterCA(t, h.svc)
|
||||
|
||||
@@ -145,6 +147,7 @@ func TestCreateDBInstance_AcceptsEnforcementTurnedOffWithNoClusterCA(t *testing.
|
||||
}
|
||||
|
||||
func TestModifyDBInstance_RefusesEnforcementWithNoClusterCA(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
require.NoError(t, err)
|
||||
@@ -163,6 +166,7 @@ func TestModifyDBInstance_RefusesEnforcementWithNoClusterCA(t *testing.T) {
|
||||
// The deferred half of a modify re-resolves at apply time, so the gate holds
|
||||
// there too rather than only at the request that recorded it.
|
||||
func TestApplyPendingModifications_RefusesEnforcementWithNoClusterCA(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newModifyHarness(t)
|
||||
h.agent.replyWith("")
|
||||
_, err := h.svc.CreateDBParameterGroup(t.Context(), parameterGroupInput(testParameterGroup), testAccountID)
|
||||
@@ -182,6 +186,7 @@ func TestApplyPendingModifications_RefusesEnforcementWithNoClusterCA(t *testing.
|
||||
}
|
||||
|
||||
func TestRestoreDBInstanceFromDBSnapshot_RefusesEnforcementWithNoClusterCA(t *testing.T) {
|
||||
t.Parallel()
|
||||
h := newSnapshotHarness(t, false)
|
||||
h.seedSnapshot(t)
|
||||
withoutClusterCA(t, h.svc)
|
||||
|
||||
@@ -21,6 +21,7 @@ func testAgentUserDataInput() agentUserDataInput {
|
||||
}
|
||||
|
||||
func TestBuildAgentUserData(t *testing.T) {
|
||||
t.Parallel()
|
||||
out := buildAgentUserData(testAgentUserDataInput())
|
||||
|
||||
require.True(t, strings.HasPrefix(out, "#cloud-config\n"), "cloud-init only parses a document with the header")
|
||||
@@ -51,6 +52,7 @@ func TestBuildAgentUserData(t *testing.T) {
|
||||
// the agent's gateway credentials come from IMDS — so nothing secret is written
|
||||
// to a data source any process on the VM can read.
|
||||
func TestBuildAgentUserDataCarriesNoCredentials(t *testing.T) {
|
||||
t.Parallel()
|
||||
in := testAgentUserDataInput()
|
||||
out := buildAgentUserData(in)
|
||||
|
||||
@@ -63,6 +65,7 @@ func TestBuildAgentUserDataCarriesNoCredentials(t *testing.T) {
|
||||
// fails the pin in a way that looks like a certificate problem rather than a
|
||||
// missing configuration.
|
||||
func TestBuildAgentUserDataOmitsAnAbsentCA(t *testing.T) {
|
||||
t.Parallel()
|
||||
in := testAgentUserDataInput()
|
||||
in.GatewayCACert = ""
|
||||
|
||||
|
||||
@@ -12,12 +12,14 @@ import (
|
||||
)
|
||||
|
||||
func TestSystemVPCNameIsPerRegion(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, "rds-system-ap-southeast-2", SystemVPCName("ap-southeast-2"))
|
||||
assert.NotEqual(t, SystemVPCName("us-east-1"), SystemVPCName("ap-southeast-2"),
|
||||
"one system VPC per region, so two regions must not resolve to the same VPC")
|
||||
}
|
||||
|
||||
func TestSystemVPCSpecDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
spec := SystemVPCSpec(nil, "ap-southeast-2")
|
||||
|
||||
assert.Equal(t, handlers_systemvpc.Spec{
|
||||
@@ -39,6 +41,7 @@ func TestSystemVPCSpecDefaults(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSystemVPCSpecHonoursOperatorOverrides(t *testing.T) {
|
||||
t.Parallel()
|
||||
spec := SystemVPCSpec(&config.RDSConfig{
|
||||
SystemVPCSupernet: "172.16.0.0/14",
|
||||
SystemVPCPrivateSubnets: 3,
|
||||
@@ -50,6 +53,7 @@ func TestSystemVPCSpecHonoursOperatorOverrides(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSystemVPCTagsAreRDSOwn(t *testing.T) {
|
||||
t.Parallel()
|
||||
spec := SystemVPCSpec(nil, "ap-southeast-2")
|
||||
|
||||
// The tag keys are the whole isolation mechanism: EKS's teardown and its
|
||||
@@ -62,6 +66,7 @@ func TestSystemVPCTagsAreRDSOwn(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSystemVPCSupernetIsUsableAndDisjointFromEKS(t *testing.T) {
|
||||
t.Parallel()
|
||||
supernet, err := netip.ParsePrefix(config.RDSDefaultSystemVPCSupernet)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, supernet.Masked(), supernet, "a supernet with host bits set is rejected by the builder")
|
||||
|
||||
Reference in New Issue
Block a user