package providerhealth import ( "strings" "testing" "time" "unicode/utf8" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" crawlv1alpha1 "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/api/v1alpha1" "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider" ) func readyAt(name string, at time.Time) crawlv1alpha1.ProviderTarget { return crawlv1alpha1.ProviderTarget{Name: name, Ready: true, Reason: provider.ReasonReachable, LastTransitionTime: metav1.NewTime(at)} } func TestBuildStatus(t *testing.T) { t.Parallel() tests := []struct { name string prev crawlv1alpha1.ProviderHealthStatus result Result wantStatus metav1.ConditionStatus wantReason string wantMessage string wantTargetsReady string // wantTransitions is each target's lastTransitionTime, in order. wantTransitions []time.Time }{ { name: "all ready", result: Result{Type: "kvm", Supported: true, Targets: []provider.TargetStatus{up("nas1"), up("nas2")}, CheckedAt: t1}, wantStatus: metav1.ConditionTrue, wantReason: reasonAllTargetsReady, wantMessage: "2 of 2 targets ready", wantTargetsReady: "2/2", wantTransitions: []time.Time{t1, t1}, }, { name: "one target down", result: Result{Type: "kvm", Supported: true, Targets: []provider.TargetStatus{up("nas1"), down("nas2", provider.ReasonUnreachable)}, CheckedAt: t1}, wantStatus: metav1.ConditionFalse, wantReason: reasonTargetsNotReady, wantMessage: "nas2: Unreachable", wantTargetsReady: "1/2", wantTransitions: []time.Time{t1, t1}, }, { name: "only a flip moves a target's transition time", prev: crawlv1alpha1.ProviderHealthStatus{Targets: []crawlv1alpha1.ProviderTarget{readyAt("nas1", t0), readyAt("nas2", t0)}}, result: Result{Type: "kvm", Supported: true, CheckedAt: t1, Targets: []provider.TargetStatus{up("nas1"), down("nas2", provider.ReasonTimeout)}}, wantStatus: metav1.ConditionFalse, wantReason: reasonTargetsNotReady, wantMessage: "nas2: Timeout", wantTargetsReady: "1/2", wantTransitions: []time.Time{t0, t1}, }, { name: "no checker", result: Result{Type: "gcp", CheckedAt: t1}, wantStatus: metav1.ConditionUnknown, wantReason: reasonChecksNotImplemented, }, { name: "nothing to check", result: Result{Type: "kvm", Supported: true, CheckedAt: t1}, wantStatus: metav1.ConditionUnknown, wantReason: reasonNoTargets, wantTargetsReady: "0/0", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() got := buildStatus(tc.prev, tc.result) cond := apimeta.FindStatusCondition(got.Conditions, crawlv1alpha1.ConditionReady) if cond == nil || cond.Status != tc.wantStatus || cond.Reason != tc.wantReason { t.Fatalf("Ready = %+v, want %s/%s", cond, tc.wantStatus, tc.wantReason) } if tc.wantMessage != "" && cond.Message != tc.wantMessage { t.Errorf("Ready message = %q, want %q", cond.Message, tc.wantMessage) } if got.TargetsReady != tc.wantTargetsReady { t.Errorf("targetsReady = %q, want %q", got.TargetsReady, tc.wantTargetsReady) } if got.Type != tc.result.Type || got.LastCheckTime == nil || !got.LastCheckTime.Time.Equal(t1) { t.Errorf("type/lastCheckTime = %q/%v, want %q/%s", got.Type, got.LastCheckTime, tc.result.Type, t1) } if len(got.Targets) != len(tc.wantTransitions) { t.Fatalf("got %d targets, want %d", len(got.Targets), len(tc.wantTransitions)) } for i, want := range tc.wantTransitions { if lt := got.Targets[i].LastTransitionTime.Time; !lt.Equal(want) { t.Errorf("target %s lastTransitionTime = %s, want %s", got.Targets[i].Name, lt, want) } } }) } } func TestBuildStatus_conditionKeepsTransitionWhileUnchanged(t *testing.T) { t.Parallel() first := buildStatus(crawlv1alpha1.ProviderHealthStatus{}, Result{Type: "kvm", Supported: true, Targets: []provider.TargetStatus{up("nas1")}, CheckedAt: t0}) firstCond := apimeta.FindStatusCondition(first.Conditions, crawlv1alpha1.ConditionReady) second := buildStatus(first, Result{Type: "kvm", Supported: true, Targets: []provider.TargetStatus{up("nas1")}, CheckedAt: t1}) cond := apimeta.FindStatusCondition(second.Conditions, crawlv1alpha1.ConditionReady) if !cond.LastTransitionTime.Time.Equal(t0) { t.Errorf("Ready lastTransitionTime = %s, want %s from the first round", cond.LastTransitionTime, t0) } if !firstCond.LastTransitionTime.Time.Equal(t0) || len(first.Conditions) != 1 { t.Errorf("buildStatus modified the previous status: %+v", first.Conditions) } } func TestBuildStatus_truncatesLongMessages(t *testing.T) { t.Parallel() long := strings.Repeat("é", 3000) got := buildStatus(crawlv1alpha1.ProviderHealthStatus{}, Result{Type: "kvm", Supported: true, CheckedAt: t1, Targets: []provider.TargetStatus{{Name: "nas1", Reason: provider.ReasonUnreachable, Message: long}}}) if n := utf8.RuneCountInString(got.Targets[0].Message); n != maxMessageLen { t.Errorf("message has %d characters, want %d", n, maxMessageLen) } if !strings.HasSuffix(got.Targets[0].Message, "…") { t.Error("truncated message does not end with an ellipsis") } }