package mqtt import ( "context" "log/slog" "testing" "time" "gitea.lab.audasmedia.com.au/sam/internet_use_control/internal/store" ) // fakes for the link's collaborators. type fakeDevices struct{ dev store.Device } func (f fakeDevices) DeviceByConfigTopic(context.Context, string) (*store.Device, error) { d := f.dev return &d, nil } type fakeReports struct{ presence bool } func (f *fakeReports) SetPresence(_ context.Context, _ int64, online bool, _ time.Time, _ string) (bool, error) { f.presence = online return true, nil } func (f *fakeReports) InsertTelemetry(context.Context, int64, string, []store.TelemetryEntry) (int, error) { return 0, nil } type fakeSource struct{} func (fakeSource) ListDevices(context.Context) ([]store.Device, error) { return nil, nil } func (fakeSource) EffectiveState(context.Context, store.Device, time.Time) (store.EffectiveState, error) { return store.EffectiveState{}, nil } // fakeMessage satisfies paho.Message for a received report. type fakeMessage struct { topic string payload []byte } func (m fakeMessage) Duplicate() bool { return false } func (m fakeMessage) Qos() byte { return 1 } func (m fakeMessage) Retained() bool { return true } func (m fakeMessage) Topic() string { return m.topic } func (m fakeMessage) MessageID() uint16 { return 0 } func (m fakeMessage) Payload() []byte { return m.payload } func (m fakeMessage) Ack() {} func newTestLink(t *testing.T, dev store.Device, reports *fakeReports, onTamper func(context.Context, store.Device, Tamper)) *Link { t.Helper() l, err := New(Options{ Broker: "tcp://broker.invalid:1883", Log: slog.New(slog.DiscardHandler), Devices: fakeDevices{dev: dev}, Source: fakeSource{}, Reports: reports, OnTamper: onTamper, }) if err != nil { t.Fatalf("New: %v", err) } // The paho client itself is never used by handleStatusMessage. l.client = nil return l } func TestStatusTamperReachesTheSink(t *testing.T) { dev := store.Device{ID: 1, Name: "Harry's phone", Topic: "restrict/harry_phone_1/config"} reports := &fakeReports{} var got Tamper var calls int l := newTestLink(t, dev, reports, func(_ context.Context, d store.Device, tamper Tamper) { calls++ got = tamper if d.ID != dev.ID { t.Errorf("device id = %d, want %d", d.ID, dev.ID) } }) payload := `{"online":true,"tamper":{"kind":"accessibility_disabled","detail":"the service was destroyed"}}` l.handleStatusMessage(nil, fakeMessage{topic: "restrict/harry_phone_1/status", payload: []byte(payload)}) if calls != 1 { t.Fatalf("OnTamper calls = %d, want 1", calls) } if got.Kind != "accessibility_disabled" { t.Errorf("kind = %q", got.Kind) } if !reports.presence { t.Error("presence was not recorded alongside the tamper") } } func TestPlainStatusDoesNotTamper(t *testing.T) { dev := store.Device{ID: 2, Name: "Finn's phone", Topic: "restrict/finn_phone_2/config"} reports := &fakeReports{} l := newTestLink(t, dev, reports, func(context.Context, store.Device, Tamper) { t.Error("OnTamper called for a plain status") }) l.handleStatusMessage(nil, fakeMessage{topic: "restrict/finn_phone_2/status", payload: []byte(`{"online":true}`)}) if !reports.presence { t.Error("presence was not recorded") } }