test(mariadb): cover the absent Complete condition and expired deadlines

Every first reconcile after the operator Backup or Restore is created
sees no Complete condition, and the guards that keep that case from
dereferencing nil could be dropped with the outcome tables staying
green. The tables now also cover the deadline failure for a run that
never reports a known reason, and read the job status back from the
API, so a lost status write fails them instead of passing on the
in-memory object.

Assisted-by: LLM
Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
This commit is contained in:
Alexey Artamonov committed 2026-11-08 20:18:29 +03:00
1 parent ccac0e17e9
commit 7e1fc7173c
1 file changed
+79 -43
@@ -871,30 +871,64 @@ func TestReconcileMariaDBRestore_KindCheckedBeforeSrcBackupGet(t *testing.T) {
// JobFailed (SetCompleteWithJob in mariadb-operator 25.10.2), so the
// Complete status alone does not tell success from failure.
var mariadbCompleteOutcomeCases = []struct {
name string
status metav1.ConditionStatus
reason string
wantFailed bool
wantDone bool
name string
noCondition bool
status metav1.ConditionStatus
reason string
message string
expired bool
wantFailed bool
wantDone bool
wantMessage []string
}{
{name: "Complete=True/JobComplete succeeds", status: metav1.ConditionTrue, reason: mariadbtypes.ConditionReasonJobComplete, wantDone: true},
{name: "Complete=True/JobFailed fails", status: metav1.ConditionTrue, reason: mariadbtypes.ConditionReasonJobFailed, wantFailed: true},
{name: "Complete=False/JobFailed fails", status: metav1.ConditionFalse, reason: mariadbtypes.ConditionReasonJobFailed, wantFailed: true},
{name: "Complete=True with an unknown reason keeps waiting", status: metav1.ConditionTrue, reason: "SomethingElse"},
{name: "Complete=False/JobComplete keeps waiting", status: metav1.ConditionFalse, reason: mariadbtypes.ConditionReasonJobComplete},
{name: "Complete=False/JobRunning keeps waiting", status: metav1.ConditionFalse, reason: mariadbtypes.ConditionReasonJobRunning},
{name: "Complete=True/JobComplete succeeds", status: metav1.ConditionTrue, reason: mariadbtypes.ConditionReasonJobComplete, message: "Success", wantDone: true},
{name: "Complete=True/JobFailed fails", status: metav1.ConditionTrue, reason: mariadbtypes.ConditionReasonJobFailed, message: "Failed", wantFailed: true,
wantMessage: []string{"tenant/op"}},
{name: "Complete=False/JobFailed fails", status: metav1.ConditionFalse, reason: mariadbtypes.ConditionReasonJobFailed, message: "Failed", wantFailed: true,
wantMessage: []string{"tenant/op"}},
{name: "Complete=True with an unknown reason keeps waiting", status: metav1.ConditionTrue, reason: "SomethingElse", message: "Something"},
{name: "Complete=False/JobComplete keeps waiting", status: metav1.ConditionFalse, reason: mariadbtypes.ConditionReasonJobComplete, message: "Success"},
{name: "Complete=False/JobRunning keeps waiting", status: metav1.ConditionFalse, reason: mariadbtypes.ConditionReasonJobRunning, message: "Running"},
{name: "no Complete condition keeps waiting", noCondition: true},
{name: "no Complete condition past the deadline fails", noCondition: true, expired: true, wantFailed: true,
wantMessage: []string{"no Complete condition observed"}},
{name: "Complete=True with an unknown reason past the deadline fails", status: metav1.ConditionTrue, reason: "SomethingElse", message: "Something", expired: true, wantFailed: true,
wantMessage: []string{"Complete=True/SomethingElse"}},
}
func mariadbCompleteCondition(status metav1.ConditionStatus, reason string) []metav1.Condition {
func mariadbCompleteConditions(noCondition bool, status metav1.ConditionStatus, reason, message string) []metav1.Condition {
if noCondition {
return nil
}
return []metav1.Condition{{
Type: mariadbtypes.ConditionTypeComplete,
Status: status,
Reason: reason,
Message: "Failed",
Message: message,
LastTransitionTime: metav1.Now(),
}}
}
// mariadbOutcomeStartedAt returns a StartedAt that is fresh, or older than
// both the backup and the default restore deadline.
func mariadbOutcomeStartedAt(expired bool) *metav1.Time {
if !expired {
now := metav1.Now()
return &now
}
past := metav1.NewTime(time.Now().Add(-2 * max(mariadbDefaultBackupDeadline, mariadbDefaultRestoreDeadline)))
return &past
}
func assertMariaDBFailureMessage(t *testing.T, message string, want []string) {
t.Helper()
for _, w := range want {
if !strings.Contains(message, w) {
t.Errorf("failure message must contain %q, got %q", w, message)
}
}
}
func TestReconcileMariaDBBackup_CompleteConditionOutcome(t *testing.T) {
strategyAPIGroup := strategyv1alpha1.GroupVersion.Group
resolved := &ResolvedBackupConfig{
@@ -907,9 +941,8 @@ func TestReconcileMariaDBBackup_CompleteConditionOutcome(t *testing.T) {
for _, tc := range mariadbCompleteOutcomeCases {
t.Run(tc.name, func(t *testing.T) {
now := metav1.Now()
seed := newMariaDBBackupJob("bj", "tenant")
seed.Status.StartedAt = &now
seed.Status.StartedAt = mariadbOutcomeStartedAt(tc.expired)
strategy := &strategyv1alpha1.MariaDB{
ObjectMeta: metav1.ObjectMeta{Name: "mdb-strategy"},
Spec: strategyv1alpha1.MariaDBSpec{Template: *newRenderedMariaDBTemplate()},
@@ -920,13 +953,13 @@ func TestReconcileMariaDBBackup_CompleteConditionOutcome(t *testing.T) {
operatorBackup := &mariadbtypes.Backup{
ObjectMeta: metav1.ObjectMeta{
Namespace: "tenant",
Name: "bj-op",
Name: "op",
Labels: map[string]string{
backupsv1alpha1.OwningJobNameLabel: seed.Name,
backupsv1alpha1.OwningJobNamespaceLabel: seed.Namespace,
},
},
Status: mariadbtypes.BackupStatus{Conditions: mariadbCompleteCondition(tc.status, tc.reason)},
Status: mariadbtypes.BackupStatus{Conditions: mariadbCompleteConditions(tc.noCondition, tc.status, tc.reason, tc.message)},
}
c := newMariaDBStrategyTestClient(t, seed, newMariaDBApp(seed.Spec.ApplicationRef.Name, "tenant"),
strategy, operatorMariaDB, operatorBackup)
@@ -941,31 +974,33 @@ func TestReconcileMariaDBBackup_CompleteConditionOutcome(t *testing.T) {
t.Fatalf("reconcileMariaDB: %v", err)
}
stored := &backupsv1alpha1.BackupJob{}
if err := c.Get(context.Background(), client.ObjectKeyFromObject(seed), stored); err != nil {
t.Fatalf("persisted Get: %v", err)
}
artifacts := &backupsv1alpha1.BackupList{}
if err := c.List(context.Background(), artifacts, client.InNamespace("tenant")); err != nil {
t.Fatalf("list Backups: %v", err)
}
switch {
case tc.wantFailed:
if bj.Status.Phase != backupsv1alpha1.BackupJobPhaseFailed {
t.Errorf("expected Phase=Failed, got %q", bj.Status.Phase)
if stored.Status.Phase != backupsv1alpha1.BackupJobPhaseFailed {
t.Errorf("expected Phase=Failed, got %q", stored.Status.Phase)
}
if !strings.Contains(bj.Status.Message, "bj-op") {
t.Errorf("failure message must name the operator Backup, got %q", bj.Status.Message)
}
if bj.Status.BackupRef != nil || len(artifacts.Items) != 0 {
t.Errorf("a failed run must not produce a Backup: ref=%v artifacts=%d", bj.Status.BackupRef, len(artifacts.Items))
assertMariaDBFailureMessage(t, stored.Status.Message, tc.wantMessage)
if stored.Status.BackupRef != nil || len(artifacts.Items) != 0 {
t.Errorf("a failed run must not produce a Backup: ref=%v artifacts=%d", stored.Status.BackupRef, len(artifacts.Items))
}
case tc.wantDone:
if bj.Status.Phase != backupsv1alpha1.BackupJobPhaseSucceeded {
t.Errorf("expected Phase=Succeeded, got %q (message %q)", bj.Status.Phase, bj.Status.Message)
if stored.Status.Phase != backupsv1alpha1.BackupJobPhaseSucceeded {
t.Errorf("expected Phase=Succeeded, got %q (message %q)", stored.Status.Phase, stored.Status.Message)
}
if bj.Status.BackupRef == nil || len(artifacts.Items) != 1 {
t.Errorf("a successful run must produce exactly one Backup: ref=%v artifacts=%d", bj.Status.BackupRef, len(artifacts.Items))
if stored.Status.BackupRef == nil || len(artifacts.Items) != 1 {
t.Errorf("a successful run must produce exactly one Backup: ref=%v artifacts=%d", stored.Status.BackupRef, len(artifacts.Items))
}
default:
if bj.Status.Phase != backupsv1alpha1.BackupJobPhaseRunning {
t.Errorf("expected Phase=Running, got %q", bj.Status.Phase)
if stored.Status.Phase != backupsv1alpha1.BackupJobPhaseRunning {
t.Errorf("expected Phase=Running, got %q", stored.Status.Phase)
}
if res.RequeueAfter == 0 {
t.Errorf("expected a requeue while the run is in progress, got %+v", res)
@@ -983,7 +1018,6 @@ func TestReconcileMariaDBRestore_CompleteConditionOutcome(t *testing.T) {
for _, tc := range mariadbCompleteOutcomeCases {
t.Run(tc.name, func(t *testing.T) {
now := metav1.Now()
cozyBackup := &backupsv1alpha1.Backup{
ObjectMeta: metav1.ObjectMeta{Namespace: "tenant", Name: "src-cozy-bk"},
Spec: backupsv1alpha1.BackupSpec{
@@ -997,18 +1031,18 @@ func TestReconcileMariaDBRestore_CompleteConditionOutcome(t *testing.T) {
BackupRef: corev1.LocalObjectReference{Name: cozyBackup.Name},
TargetApplicationRef: &corev1.TypedLocalObjectReference{Kind: "MariaDB", Name: "target", APIGroup: &apps},
},
Status: backupsv1alpha1.RestoreJobStatus{StartedAt: &now, Phase: backupsv1alpha1.RestoreJobPhaseRunning},
Status: backupsv1alpha1.RestoreJobStatus{StartedAt: mariadbOutcomeStartedAt(tc.expired), Phase: backupsv1alpha1.RestoreJobPhaseRunning},
}
operatorRestore := &mariadbtypes.Restore{
ObjectMeta: metav1.ObjectMeta{
Namespace: "tenant",
Name: "rj-op",
Name: "op",
Labels: map[string]string{
backupsv1alpha1.OwningJobNameLabel: seed.Name,
backupsv1alpha1.OwningJobNamespaceLabel: seed.Namespace,
},
},
Status: mariadbtypes.RestoreStatus{Conditions: mariadbCompleteCondition(tc.status, tc.reason)},
Status: mariadbtypes.RestoreStatus{Conditions: mariadbCompleteConditions(tc.noCondition, tc.status, tc.reason, tc.message)},
}
c := newMariaDBStrategyTestClient(t, cozyBackup, seed, operatorRestore,
&mariadbtypes.Backup{ObjectMeta: metav1.ObjectMeta{Namespace: "tenant", Name: "src-op-bk"}},
@@ -1024,21 +1058,23 @@ func TestReconcileMariaDBRestore_CompleteConditionOutcome(t *testing.T) {
t.Fatalf("reconcileMariaDBRestore: %v", err)
}
stored := &backupsv1alpha1.RestoreJob{}
if err := c.Get(context.Background(), client.ObjectKeyFromObject(seed), stored); err != nil {
t.Fatalf("persisted Get: %v", err)
}
switch {
case tc.wantFailed:
if rj.Status.Phase != backupsv1alpha1.RestoreJobPhaseFailed {
t.Errorf("expected Phase=Failed, got %q", rj.Status.Phase)
}
if !strings.Contains(rj.Status.Message, "rj-op") {
t.Errorf("failure message must name the operator Restore, got %q", rj.Status.Message)
if stored.Status.Phase != backupsv1alpha1.RestoreJobPhaseFailed {
t.Errorf("expected Phase=Failed, got %q", stored.Status.Phase)
}
assertMariaDBFailureMessage(t, stored.Status.Message, tc.wantMessage)
case tc.wantDone:
if rj.Status.Phase != backupsv1alpha1.RestoreJobPhaseSucceeded {
t.Errorf("expected Phase=Succeeded, got %q (message %q)", rj.Status.Phase, rj.Status.Message)
if stored.Status.Phase != backupsv1alpha1.RestoreJobPhaseSucceeded {
t.Errorf("expected Phase=Succeeded, got %q (message %q)", stored.Status.Phase, stored.Status.Message)
}
default:
if rj.Status.Phase != backupsv1alpha1.RestoreJobPhaseRunning {
t.Errorf("expected Phase=Running, got %q", rj.Status.Phase)
if stored.Status.Phase != backupsv1alpha1.RestoreJobPhaseRunning {
t.Errorf("expected Phase=Running, got %q", stored.Status.Phase)
}
if res.RequeueAfter == 0 {
t.Errorf("expected a requeue while the run is in progress, got %+v", res)