Files
cacto 81040eec62 Добавлены метрики Prometheus и дашборды Grafana (этап 2.5)
Все три Go-сервиса (ingest, device-control, rule-engine) отдают
/metrics в формате Prometheus: счётчики MQTT/RabbitMQ/ClickHouse
операций, гистограммы длительности батч-флашей и диспатча правил,
переходы устройств online/offline. Добавлены сервисы prometheus и
grafana в docker-compose с провижининг конфигом (datasource +
два готовых дашборда: "Ingest & Telemetry" и "Automation & Devices").
2026-08-11 21:58:59 +05:00

152 lines
4.2 KiB
Go

package server
import (
"context"
"encoding/json"
"errors"
"io"
"log/slog"
"testing"
"github.com/prometheus/client_golang/prometheus/testutil"
devicecontrol "git.cactoz.su/cacto/home_automatization/proto/device_control"
"git.cactoz.su/cacto/home_automatization/services/device-control-service/internal/metrics"
)
type fakeShadow struct {
err error
patches []map[string]any
}
func (f *fakeShadow) PatchDesiredState(_ context.Context, _ string, patch map[string]any) error {
if f.err != nil {
return f.err
}
f.patches = append(f.patches, patch)
return nil
}
type fakeMQTT struct {
err error
topic string
payload []byte
}
func (f *fakeMQTT) Publish(topic string, _ byte, _ bool, payload []byte) error {
if f.err != nil {
return f.err
}
f.topic = topic
f.payload = payload
return nil
}
func discardLogger() *slog.Logger {
return slog.New(slog.NewTextHandler(io.Discard, nil))
}
func TestTurnOn_Success(t *testing.T) {
sh := &fakeShadow{}
mq := &fakeMQTT{}
s := New(sh, mq, discardLogger())
res, err := s.TurnOn(context.Background(), &devicecontrol.TurnOnRequest{DeviceId: "d1"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !res.Success {
t.Fatalf("expected success, got error %q", res.Error)
}
if len(sh.patches) != 1 || sh.patches[0]["power"] != "on" {
t.Fatalf("got desired patches %+v, want [{power: on}]", sh.patches)
}
if mq.topic != "devices/d1/commands" {
t.Fatalf("got topic %q, want devices/d1/commands", mq.topic)
}
var payload map[string]any
if err := json.Unmarshal(mq.payload, &payload); err != nil {
t.Fatalf("unmarshal published payload: %v", err)
}
if payload["action"] != "turn_on" {
t.Fatalf("got payload %+v, want action=turn_on", payload)
}
}
func TestSetLevel_Success(t *testing.T) {
sh := &fakeShadow{}
mq := &fakeMQTT{}
s := New(sh, mq, discardLogger())
res, err := s.SetLevel(context.Background(), &devicecontrol.SetLevelRequest{DeviceId: "d1", Level: 42.5})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !res.Success {
t.Fatalf("expected success, got error %q", res.Error)
}
if sh.patches[0]["level"] != 42.5 {
t.Fatalf("got desired patch %+v, want level=42.5", sh.patches[0])
}
}
func TestDispatch_MissingDeviceID(t *testing.T) {
s := New(&fakeShadow{}, &fakeMQTT{}, discardLogger())
res, err := s.TurnOff(context.Background(), &devicecontrol.TurnOffRequest{DeviceId: ""})
if err != nil {
t.Fatalf("unexpected transport error: %v", err)
}
if res.Success {
t.Fatal("expected failure for missing device_id")
}
}
func TestDispatch_ShadowFailure(t *testing.T) {
s := New(&fakeShadow{err: errors.New("redis down")}, &fakeMQTT{}, discardLogger())
res, err := s.TurnOn(context.Background(), &devicecontrol.TurnOnRequest{DeviceId: "d1"})
if err != nil {
t.Fatalf("unexpected transport error: %v", err)
}
if res.Success {
t.Fatal("expected failure when shadow patch errors")
}
}
func TestDispatch_PublishFailure(t *testing.T) {
s := New(&fakeShadow{}, &fakeMQTT{err: errors.New("broker unreachable")}, discardLogger())
res, err := s.TurnOn(context.Background(), &devicecontrol.TurnOnRequest{DeviceId: "d1"})
if err != nil {
t.Fatalf("unexpected transport error: %v", err)
}
if res.Success {
t.Fatal("expected failure when mqtt publish errors")
}
}
func TestDispatch_RecordsMetrics(t *testing.T) {
successCounter := metrics.CommandsTotal.WithLabelValues("turn_on", "success")
errorCounter := metrics.CommandsTotal.WithLabelValues("turn_on", "error")
before := testutil.ToFloat64(successCounter)
s := New(&fakeShadow{}, &fakeMQTT{}, discardLogger())
if _, err := s.TurnOn(context.Background(), &devicecontrol.TurnOnRequest{DeviceId: "d1"}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := testutil.ToFloat64(successCounter); got != before+1 {
t.Fatalf("got turn_on/success counter %v, want %v", got, before+1)
}
beforeErr := testutil.ToFloat64(errorCounter)
s2 := New(&fakeShadow{err: errors.New("redis down")}, &fakeMQTT{}, discardLogger())
if _, err := s2.TurnOn(context.Background(), &devicecontrol.TurnOnRequest{DeviceId: "d1"}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := testutil.ToFloat64(errorCounter); got != beforeErr+1 {
t.Fatalf("got turn_on/error counter %v, want %v", got, beforeErr+1)
}
}