From 9c4dedb0eb3ea9d4f2c64d4405e1c420d54b9c52 Mon Sep 17 00:00:00 2001 From: Valentin Knabel Date: Tue, 30 Jul 2024 14:41:28 +0200 Subject: [PATCH] test(health): status tests --- pkg/healthstatus/async-check_test.go | 141 +++++++++++++++++ pkg/healthstatus/delayed-error-check_test.go | 158 +++++++++++++++++++ pkg/healthstatus/grouped-check_test.go | 148 +++++++++++++++++ 3 files changed, 447 insertions(+) create mode 100644 pkg/healthstatus/async-check_test.go create mode 100644 pkg/healthstatus/delayed-error-check_test.go create mode 100644 pkg/healthstatus/grouped-check_test.go diff --git a/pkg/healthstatus/async-check_test.go b/pkg/healthstatus/async-check_test.go new file mode 100644 index 0000000..bf94f56 --- /dev/null +++ b/pkg/healthstatus/async-check_test.go @@ -0,0 +1,141 @@ +package healthstatus + +import ( + "context" + "errors" + "log/slog" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +type countedCheck struct { + name string + state currentState + checks int +} + +func (c *countedCheck) ServiceName() string { + return c.name +} +func (c *countedCheck) Check(context.Context) (HealthResult, error) { + c.checks++ + return c.state.status, c.state.err +} + +func TestAsync(t *testing.T) { + slog.SetLogLoggerLevel(slog.LevelDebug) + log := slog.Default() + + tests := []struct { + name string + interval time.Duration + callIntervals []time.Duration + wantChecks int + hc *countedCheck + want currentState + }{ + { + name: "succeeding call", + interval: 2 * time.Second, + callIntervals: []time.Duration{ + 500 * time.Millisecond, + 100 * time.Millisecond, + }, + wantChecks: 1, + hc: &countedCheck{ + state: currentState{ + status: HealthResult{ + Status: HealthStatusHealthy, + }, + }, + }, + want: currentState{ + status: HealthResult{ + Status: HealthStatusHealthy, + }, + }, + }, + { + name: "multiple calls", + interval: 2 * time.Second, + callIntervals: []time.Duration{ + 500 * time.Millisecond, + 600 * time.Millisecond, + }, + wantChecks: 1, + hc: &countedCheck{ + state: currentState{ + status: HealthResult{ + Status: HealthStatusHealthy, + }, + }, + }, + want: currentState{ + status: HealthResult{ + Status: HealthStatusHealthy, + }, + }, + }, + { + name: "error propagated", + interval: 2 * time.Second, + callIntervals: []time.Duration{ + 100 * time.Millisecond, + }, + wantChecks: 1, + hc: &countedCheck{ + state: currentState{ + status: HealthResult{ + Status: HealthStatusUnhealthy, + }, + err: errors.New("intentional"), + }, + }, + want: currentState{ + status: HealthResult{ + Status: HealthStatusUnhealthy, + }, + err: errors.New("intentional"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + hc := Async(log, tt.interval, tt.hc) + hc.Start(ctx) + + for _, timeout := range tt.callIntervals { + time.Sleep(timeout) + + got, gotErr := hc.Check(ctx) + + var ( + wantErrStr = "" + gotErrStr = "" + ) + if tt.want.err != nil { + wantErrStr = tt.want.err.Error() + } + if gotErr != nil { + gotErrStr = gotErr.Error() + } + if diff := cmp.Diff(wantErrStr, gotErrStr); diff != "" { + t.Errorf("mismatch error (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tt.want.status, got); diff != "" { + t.Errorf("mismatch result (-want +got):\n%s", diff) + } + + } + + if tt.hc.checks != tt.wantChecks { + t.Errorf("mismatched calls (want %d, got %d)", tt.wantChecks, tt.hc.checks) + } + }) + } +} diff --git a/pkg/healthstatus/delayed-error-check_test.go b/pkg/healthstatus/delayed-error-check_test.go new file mode 100644 index 0000000..5b25cdb --- /dev/null +++ b/pkg/healthstatus/delayed-error-check_test.go @@ -0,0 +1,158 @@ +package healthstatus + +import ( + "context" + "errors" + "testing" + + "github.com/google/go-cmp/cmp" +) + +type recordedCheck struct { + name string + states []currentState +} + +func (c *recordedCheck) ServiceName() string { + return c.name +} +func (c *recordedCheck) Check(context.Context) (HealthResult, error) { + cur := c.states[0] + if len(c.states) > 1 { + c.states = c.states[1:] + } + return cur.status, cur.err +} + +func TestDelayErrors(t *testing.T) { + tests := []struct { + name string + hc *DelayedErrorHealthCheck + want []currentState + }{ + { + name: "check always returns first result even on error", + hc: DelayErrors(1, &recordedCheck{ + name: "record", + states: []currentState{ + { + status: HealthResult{ + Status: HealthStatusUnhealthy, + Message: "intentional", + Services: map[string]HealthResult{}, + }, + err: errors.New("initial error"), + }, + { + status: HealthResult{ + Status: HealthStatusUnhealthy, + Message: "intentional", + Services: map[string]HealthResult{}, + }, + err: errors.New("secondary error"), + }, + }, + }), + want: []currentState{ + { + status: HealthResult{ + Status: HealthStatusUnhealthy, + Message: "intentional", + Services: map[string]HealthResult{}, + }, + err: errors.New("initial error"), + }, + { + status: HealthResult{ + Status: HealthStatusUnhealthy, + Message: "intentional", + Services: map[string]HealthResult{}, + }, + err: errors.New("secondary error"), + }, + }, + }, + { + name: "ignores first error after inital success", + hc: DelayErrors(1, &recordedCheck{ + name: "record", + states: []currentState{ + { + status: HealthResult{ + Status: HealthStatusHealthy, + Message: "", + Services: map[string]HealthResult{}, + }, + }, + { + status: HealthResult{ + Status: HealthStatusUnhealthy, + Message: "intentional", + Services: map[string]HealthResult{}, + }, + err: errors.New("hidden error"), + }, + { + status: HealthResult{ + Status: HealthStatusUnhealthy, + Message: "intentional", + Services: map[string]HealthResult{}, + }, + err: errors.New("presented error"), + }, + }, + }), + want: []currentState{ + { + status: HealthResult{ + Status: HealthStatusHealthy, + Message: "", + Services: map[string]HealthResult{}, + }, + }, + { + status: HealthResult{ + Status: HealthStatusHealthy, + Message: "", + Services: map[string]HealthResult{}, + }, + }, + { + status: HealthResult{ + Status: HealthStatusUnhealthy, + Message: "intentional", + Services: map[string]HealthResult{}, + }, + err: errors.New("presented error"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + for i, w := range tt.want { + got, gotErr := tt.hc.Check(ctx) + + var ( + wantErrStr = "" + gotErrStr = "" + ) + if w.err != nil { + wantErrStr = w.err.Error() + } + if gotErr != nil { + gotErrStr = gotErr.Error() + } + if diff := cmp.Diff(wantErrStr, gotErrStr); diff != "" { + t.Errorf("mismatch error on call %d (-want +got):\n%s", i, diff) + } + if diff := cmp.Diff(w.status, got); diff != "" { + t.Errorf("mismatch result on call %d (-want +got):\n%s", i, diff) + } + } + }) + } +} diff --git a/pkg/healthstatus/grouped-check_test.go b/pkg/healthstatus/grouped-check_test.go new file mode 100644 index 0000000..b8731f5 --- /dev/null +++ b/pkg/healthstatus/grouped-check_test.go @@ -0,0 +1,148 @@ +package healthstatus + +import ( + "context" + "errors" + "log/slog" + "testing" + + "github.com/google/go-cmp/cmp" +) + +type staticCheck struct { + name string + state currentState +} + +func (c *staticCheck) ServiceName() string { + return c.name +} +func (c *staticCheck) Check(context.Context) (HealthResult, error) { + return c.state.status, c.state.err +} +func TestGrouped(t *testing.T) { + slog.SetLogLoggerLevel(slog.LevelDebug) + log := slog.Default() + + tests := []struct { + name string + hcs []HealthCheck + want currentState + }{ + { + name: "adds subchecks as service by name", + hcs: []HealthCheck{ + &staticCheck{ + name: "a", + state: currentState{ + status: HealthResult{ + Status: HealthStatusHealthy, + Message: "", + Services: map[string]HealthResult{}, + }, + }, + }, + &staticCheck{ + name: "b", + state: currentState{ + status: HealthResult{ + Status: HealthStatusDegraded, + Message: "bees are tired", + Services: map[string]HealthResult{}, + }, + }, + }, + }, + want: currentState{ + status: HealthResult{ + Status: HealthStatusDegraded, + Message: "", + Services: map[string]HealthResult{ + "a": { + Status: HealthStatusHealthy, + Message: "", + Services: map[string]HealthResult{}, + }, + "b": { + Status: HealthStatusDegraded, + Message: "bees are tired", + Services: map[string]HealthResult{}, + }, + }, + }, + }, + }, + { + name: "errors if one errors", + hcs: []HealthCheck{ + &staticCheck{ + name: "a", + state: currentState{ + status: HealthResult{ + Status: HealthStatusUnhealthy, + Message: "", + Services: map[string]HealthResult{}, + }, + err: errors.New("intentional error"), + }, + }, + &staticCheck{ + name: "b", + state: currentState{ + status: HealthResult{ + Status: HealthStatusDegraded, + Message: "bees are tired", + Services: map[string]HealthResult{}, + }, + }, + }, + }, + want: currentState{ + status: HealthResult{ + Status: HealthStatusPartiallyUnhealthy, + Message: "intentional error", + Services: map[string]HealthResult{ + "a": { + Status: HealthStatusUnhealthy, + Message: "intentional error", + Services: map[string]HealthResult{}, + }, + "b": { + Status: HealthStatusDegraded, + Message: "bees are tired", + Services: map[string]HealthResult{}, + }, + }, + }, + err: errors.New("intentional error"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + group := Grouped(log, tt.name, tt.hcs...) + + got, gotErr := group.Check(ctx) + + var ( + wantErrStr = "" + gotErrStr = "" + ) + if tt.want.err != nil { + wantErrStr = tt.want.err.Error() + } + if gotErr != nil { + gotErrStr = gotErr.Error() + } + if diff := cmp.Diff(wantErrStr, gotErrStr); diff != "" { + t.Errorf("mismatch error (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tt.want.status, got); diff != "" { + t.Errorf("mismatch result (-want +got):\n%s", diff) + } + }) + } +}