Files
home_automatization/services/health-check-service/internal/shadow/store_test.go
T
cacto 17af76c159 Вынос health-check-service в отдельный сервис
Тикер офлайн-детекции (internal/healthcheck) переехал из
device-control-service в новый самостоятельный Go-сервис — как и
планировалось с самого начала (см. изначальный README-заглушку).
Детекция online остаётся в device-control-service: она происходит
как побочный эффект уже имеющейся там MQTT-подписки на
devices/+/telemetry и devices/+/ack, заводить для этого отдельный
сервис с дублирующей MQTT-подпиской избыточно.

health-check-service — свой Go-модуль (без зависимости от proto/,
сборка из собственного контекста), читает только узкое
read+offline-подмножество Device Shadow keyspace в Redis
(devices:known, status, last_seen) — полный Shadow API с записью
desired/reported state остаётся только в device-control-service.

Метрика device_control_health_transitions_total переименована в
device_health_transitions_total и теперь публикуется с этим именем
из ДВУХ сервисов (device-control-service — direction=online,
health-check-service — direction=offline): Prometheus агрегирует
одноимённые метрики с разных таргетов прозрачно, поэтому Grafana-
дашборд адаптирован простой сменой имени метрики в запросе, без
переделки панели.

Проверено вживую через docker compose: реальный online→offline
переход (публикация тестовой телеметрии + ожидание таймаута)
корректно долетает до Redis и RabbitMQ (device.status_changed),
Prometheus видит новый scrape-таргет как up.
2026-08-13 11:24:25 +05:00

94 lines
2.4 KiB
Go

package shadow
import (
"context"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
)
func newTestStore(t *testing.T) *Store {
t.Helper()
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
t.Cleanup(func() { rdb.Close() })
return New(rdb)
}
// markOnline seeds Redis the way device-control-service's Touch() would,
// without depending on that service's code.
func markOnline(t *testing.T, s *Store, deviceID string, lastSeen time.Time) {
t.Helper()
ctx := context.Background()
if err := s.rdb.SAdd(ctx, knownDevicesKey, deviceID).Err(); err != nil {
t.Fatalf("seed known device: %v", err)
}
if err := s.rdb.Set(ctx, statusKey(deviceID), StatusOnline, 0).Err(); err != nil {
t.Fatalf("seed status: %v", err)
}
if err := s.rdb.Set(ctx, lastSeenKey(deviceID), lastSeen.Unix(), 0).Err(); err != nil {
t.Fatalf("seed last_seen: %v", err)
}
}
func TestKnownDevices_ListsSeededDevices(t *testing.T) {
ctx := context.Background()
s := newTestStore(t)
markOnline(t, s, "d1", time.Now())
markOnline(t, s, "d2", time.Now())
got, err := s.KnownDevices(ctx)
if err != nil {
t.Fatalf("known devices: %v", err)
}
if len(got) != 2 {
t.Fatalf("got %d known devices, want 2 (%v)", len(got), got)
}
}
func TestMarkOfflineIfStale(t *testing.T) {
ctx := context.Background()
s := newTestStore(t)
markOnline(t, s, "d1", time.Now())
changed, err := s.MarkOfflineIfStale(ctx, "d1", time.Hour)
if err != nil {
t.Fatalf("mark offline (not stale): %v", err)
}
if changed {
t.Fatal("device just touched should not be considered stale")
}
changed, err = s.MarkOfflineIfStale(ctx, "d1", -time.Second) // any age counts as stale
if err != nil {
t.Fatalf("mark offline (stale): %v", err)
}
if !changed {
t.Fatal("expected status to flip to offline")
}
// Already offline: a second call should report no further change.
changed, err = s.MarkOfflineIfStale(ctx, "d1", -time.Second)
if err != nil {
t.Fatalf("mark offline (already offline): %v", err)
}
if changed {
t.Fatal("expected no change once already offline")
}
}
func TestMarkOfflineIfStale_UnknownDeviceIsNoop(t *testing.T) {
ctx := context.Background()
s := newTestStore(t)
changed, err := s.MarkOfflineIfStale(ctx, "ghost", time.Second)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if changed {
t.Fatal("unknown device should never be reported as changed")
}
}