Files
home_automatization/services/device-control-service/internal/healthcheck/checker_test.go
T
cacto 3eb84aae6c Реализация device-control-service: gRPC + MQTT + Device Shadow в Redis
Добавлен gRPC-контракт proto/device_control (TurnOn/TurnOff/SetLevel),
общий Go-модуль proto/ для переиспользования сгенерированного кода.
device-control-service принимает команды по gRPC, обновляет desired_state
в Redis и публикует их в MQTT; слушает devices/+/telemetry и devices/+/ack
для отметки живости устройства и обновления reported_state; горутина
health-check переводит устройство в offline по таймауту и публикует
событие в RabbitMQ (device.status_changed) для будущего notification-service.

Проверено сквозным тестом через docker compose: grpcurl TurnOn/SetLevel
меняет desired_state и уходит в MQTT, mosquitto_pub с ack обновляет
reported_state и статус, health-check автоматически переводит устройство
в offline по истечении таймаута.
2026-07-26 18:40:36 +05:00

77 lines
1.8 KiB
Go

package healthcheck
import (
"context"
"io"
"log/slog"
"sync"
"testing"
"time"
)
type fakeStore struct {
mu sync.Mutex
known []string
stale map[string]bool // deviceID -> whether MarkOfflineIfStale should report a change
changes []string // devices actually marked changed, in call order
}
func (f *fakeStore) KnownDevices(context.Context) ([]string, error) {
f.mu.Lock()
defer f.mu.Unlock()
return append([]string(nil), f.known...), nil
}
func (f *fakeStore) MarkOfflineIfStale(_ context.Context, deviceID string, _ time.Duration) (bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
if !f.stale[deviceID] {
return false, nil
}
// Only report the transition once, like the real store would.
f.stale[deviceID] = false
f.changes = append(f.changes, deviceID)
return true, nil
}
func discardLogger() *slog.Logger {
return slog.New(slog.NewTextHandler(io.Discard, nil))
}
func TestChecker_FlipsStaleDevicesAndCallsOnOffline(t *testing.T) {
store := &fakeStore{
known: []string{"d1", "d2"},
stale: map[string]bool{"d1": true, "d2": false},
}
offline := make(chan string, 2)
c := New(store, time.Minute, 10*time.Millisecond, func(deviceID string) {
offline <- deviceID
}, discardLogger())
c.Start()
defer c.Stop()
select {
case id := <-offline:
if id != "d1" {
t.Fatalf("got offline callback for %q, want d1", id)
}
case <-time.After(time.Second):
t.Fatal("timed out waiting for onOffline callback")
}
select {
case id := <-offline:
t.Fatalf("unexpected second onOffline callback for %q (d2 was never stale)", id)
case <-time.After(50 * time.Millisecond):
// expected: no further callbacks
}
}
func TestChecker_StopWaitsForLoopExit(t *testing.T) {
store := &fakeStore{known: nil}
c := New(store, time.Minute, time.Hour, nil, discardLogger())
c.Start()
c.Stop() // must return without hanging
}