sub2api/backend/internal/service/health_service_test.go
win 110902ad4b feat(health): split liveness and readiness probes
Add HealthService with Liveness (no-op) and Readiness (DB+Redis ping
with per-component timeout) checks. Expose three endpoints:

- /healthz : new liveness endpoint, zero-dependency, always 200
- /ready   : new readiness endpoint, returns 503 with details on dep
             failure; suitable for K8s readinessProbe and load balancers
- /health  : preserved for backward compatibility, equivalent to
             /healthz

Switch primary docker-compose healthcheck to /ready so the container
is only marked healthy once DB+Redis are reachable. Standalone/dev/
local compose files keep /health to avoid disrupting existing setups.

Tests: unit tests cover liveness, readiness with both deps healthy,
each dep failing independently, and per-component timeout enforcement.
2026-04-28 23:39:50 +08:00

94 lines
2.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"context"
"database/sql"
"errors"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
)
func TestHealthService_Liveness_AlwaysOK(t *testing.T) {
s := NewHealthService(nil, nil)
require.NoError(t, s.Liveness())
}
func TestHealthService_Readiness_AllNilReturnsOK(t *testing.T) {
// 当所有依赖都为 nil 时(早期启动或 unit testreadiness 应直接 OK。
s := NewHealthService(nil, nil)
report := s.Readiness(context.Background())
require.True(t, report.OK)
require.Empty(t, report.Details)
}
func TestHealthService_Readiness_DBPingFails(t *testing.T) {
db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
require.NoError(t, err)
defer db.Close()
mock.ExpectPing().WillReturnError(errors.New("connection refused"))
s := NewHealthService(db, nil)
report := s.Readiness(context.Background())
require.False(t, report.OK)
require.Contains(t, report.Details, "database")
require.False(t, report.Details["database"].OK)
require.Contains(t, report.Details["database"].Error, "connection refused")
}
func TestHealthService_Readiness_DBOK(t *testing.T) {
db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
require.NoError(t, err)
defer db.Close()
mock.ExpectPing()
s := NewHealthService(db, nil)
report := s.Readiness(context.Background())
require.True(t, report.OK)
require.True(t, report.Details["database"].OK)
}
func TestHealthService_Readiness_RedisFails(t *testing.T) {
// 指向一个不可达端口让 redis ping 立刻失败。
rdb := redis.NewClient(&redis.Options{
Addr: "127.0.0.1:1",
DialTimeout: 200 * time.Millisecond,
ReadTimeout: 200 * time.Millisecond,
})
defer rdb.Close()
s := NewHealthService(nil, rdb)
s.timeout = 500 * time.Millisecond
report := s.Readiness(context.Background())
require.False(t, report.OK)
require.Contains(t, report.Details, "redis")
require.False(t, report.Details["redis"].OK)
}
func TestHealthService_Readiness_PerComponentTimeout(t *testing.T) {
// 验证 readiness 在超时时不会无限挂住。
db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
require.NoError(t, err)
defer db.Close()
mock.ExpectPing().WillDelayFor(2 * time.Second)
s := NewHealthService(db, nil)
s.timeout = 100 * time.Millisecond
start := time.Now()
report := s.Readiness(context.Background())
elapsed := time.Since(start)
require.Less(t, elapsed, 1*time.Second, "readiness should respect per-component timeout")
require.False(t, report.OK)
require.NotEmpty(t, report.Details["database"].Error, "timeout should propagate as an error")
}
// 抑制未使用包警告database/sql 在签名里使用)。
var _ = sql.ErrNoRows