package healthcheck import ( "bytes" "context" "errors" "io" "net/http" "sync/atomic" "testing" "time" ) const healthyActuatorSample = `{"status":"UP","components":{"db":{"status":"UP","components":{"dorisDataSource":{"status":"UP"},"postgresqlDataSource":{"status":"UP"}}},"diskSpace":{"status":"UP"},"ping":{"status":"UP"},"redis":{"status":"UP"}}}` func TestActuatorCheckerAcceptsConfirmedResponseShape(t *testing.T) { t.Parallel() client := newHTTPClient(func(request *http.Request) *http.Response { if request.URL.Path != "/yms/actuator/health" { t.Errorf("unexpected health path: %s", request.URL.Path) } return response(http.StatusOK, healthyActuatorSample) }) checker := newTestChecker(t, client) report, err := checker.Wait(context.Background(), "http://127.0.0.1:8081/yms/actuator/health", time.Second, alwaysRunning) if err != nil { t.Fatalf("wait for healthy Actuator: %v", err) } if report.Status != "UP" || report.Components["db"] != "UP" || report.Components["redis"] != "UP" { t.Fatalf("unexpected health report: %+v", report) } } func TestActuatorCheckerChecksOnceForTransactionRecovery(t *testing.T) { t.Parallel() client := newHTTPClient(func(*http.Request) *http.Response { return response(http.StatusOK, healthyActuatorSample) }) checker := newTestChecker(t, client) report, ready, err := checker.Check(context.Background(), "http://127.0.0.1:8081/yms/actuator/health", alwaysRunning) if err != nil || !ready || report.Status != "UP" { t.Fatalf("unexpected one-shot health result: report=%+v ready=%t err=%v", report, ready, err) } } func TestActuatorCheckerSamplesReadinessWithoutRestarting(t *testing.T) { t.Parallel() var requests atomic.Int32 client := newHTTPClient(func(*http.Request) *http.Response { if requests.Add(1) < 3 { return response(http.StatusServiceUnavailable, `{"status":"DOWN"}`) } return response(http.StatusOK, healthyActuatorSample) }) checker := newTestChecker(t, client) if _, err := checker.Wait(context.Background(), "http://127.0.0.1:8081/yms/actuator/health", time.Second, alwaysRunning); err != nil { t.Fatalf("wait for delayed readiness: %v", err) } if requests.Load() != 3 { t.Fatalf("unexpected request count: %d", requests.Load()) } } func TestActuatorCheckerStopsImmediatelyWhenContainerStops(t *testing.T) { t.Parallel() checker := newTestChecker(t, http.DefaultClient) _, err := checker.Wait(context.Background(), "http://127.0.0.1:8081/yms/actuator/health", time.Second, func(context.Context) (bool, error) { return false, nil }) if !errors.Is(err, ErrWorkloadStopped) { t.Fatalf("expected stopped workload error, got %v", err) } } func TestActuatorCheckerHonorsOverallTimeout(t *testing.T) { t.Parallel() client := newHTTPClient(func(*http.Request) *http.Response { return response(http.StatusServiceUnavailable, `{"status":"DOWN"}`) }) checker := newTestChecker(t, client) _, err := checker.Wait(context.Background(), "http://127.0.0.1:8081/yms/actuator/health", 25*time.Millisecond, alwaysRunning) if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("expected deadline error, got %v", err) } } func newTestChecker(t *testing.T, client *http.Client) *ActuatorChecker { t.Helper() checker, err := NewActuatorChecker(client, 5*time.Millisecond) if err != nil { t.Fatalf("create Actuator checker: %v", err) } return checker } func alwaysRunning(context.Context) (bool, error) { return true, nil } type roundTripFunc func(*http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { return f(request) } func newHTTPClient(handle func(*http.Request) *http.Response) *http.Client { return &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) { return handle(request), nil })} } func response(statusCode int, body string) *http.Response { return &http.Response{ StatusCode: statusCode, Header: make(http.Header), Body: io.NopCloser(bytes.NewBufferString(body)), } }