Реализация 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 по истечении таймаута.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
devicecontrol "git.cactoz.su/cacto/home_automatization/proto/device_control"
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user