package integration import ( "context" "strings" "testing" "time" "github.com/eclipse/paho.golang/paho" "robot-market/test-mqtt-5/internal/backend" "robot-market/test-mqtt-5/internal/contracts" "robot-market/test-mqtt-5/internal/events" "robot-market/test-mqtt-5/internal/mqtt" ) func TestRemoteBrokerQueuedMessageExpiry(t *testing.T) { deviceCfg := remoteConfig(t, "sim-001", false) deviceCfg.MQTT.ClientID = "rm5-expiry-" + backend.ID()[:12] ctx, cancel := context.WithCancel(context.Background()) defer cancel() device := mqtt.New(deviceCfg, store(deviceCfg)) if err := device.Start(ctx, []string{"commands"}, nil); err != nil { t.Fatal(err) } if err := device.Close(); err != nil { t.Fatal(err) } serverCfg := remoteConfig(t, "test-backend", false) server := mqtt.New(serverCfg, store(serverCfg)) if err := server.Start(ctx, []string{"acks"}, nil); err != nil { t.Fatal(err) } defer server.Close() // Application deadline remains valid; the broker must expire this queued packet. p := mqtt.Packet(mqtt.Topic(serverCfg, "commands"), 1, false, backend.NewCommand("sim-001", "ping", backend.ID(), 15*time.Second)) p.Properties.MessageExpiry = paho.Uint32(1) if err := server.Publish(ctx, p); err != nil { t.Fatal(err) } time.Sleep(2 * time.Second) deviceCfg.MQTT.CleanStart = false device = mqtt.New(deviceCfg, store(deviceCfg)) if err := device.Start(ctx, []string{"commands"}, nil); err != nil { t.Fatal(err) } defer device.Close() history, _ := device.Events.Snapshot() resumed := false for _, e := range history { if e.Kind == events.ConnectionUp && strings.Contains(e.Outcome, "session_present=true") { resumed = true } } if !resumed { t.Fatal("offline subscriber session was lost; expiry test inconclusive") } select { case <-device.Incoming: t.Fatal("broker delivered expired queued command") case <-time.After(300 * time.Millisecond): } fresh := mqtt.Packet(mqtt.Topic(serverCfg, "commands"), 1, false, backend.NewCommand("sim-001", "ping", backend.ID(), 15*time.Second)) if err := server.Publish(ctx, fresh); err != nil { t.Fatal(err) } select { case got := <-device.Incoming: if string(got.Payload) != string(fresh.Payload) { t.Fatal("wrong queued packet") } case <-time.After(3 * time.Second): t.Fatal("fresh control command missing") } t.Log("broker expiry distinct from valid application deadline, persistent offline queue, fresh control delivery: PASS") } func TestRemoteWillAfterAbruptDisconnect(t *testing.T) { observerCfg := remoteConfig(t, "test-backend", false) observer := mqtt.New(observerCfg, store(observerCfg)) ctx, cancel := context.WithCancel(context.Background()) defer cancel() if err := observer.Start(ctx, []string{"status"}, nil); err != nil { t.Fatal(err) } defer observer.Close() cfg := remoteConfig(t, "sim-001", false) proxy := newRelay(t, strings.TrimPrefix(cfg.MQTT.URL, "tls://")) defer proxy.close() cfg.MQTT.URL = "tls://" + proxy.listener.Addr().String() cfg.MQTT.ReconnectMin = 100 * time.Millisecond cfg.MQTT.ReconnectMax = 200 * time.Millisecond boot := "will-" + backend.ID()[:12] status := contracts.Status{Envelope: contracts.Envelope{SchemaVersion: 1, DeviceID: "sim-001"}, BootID: boot, State: "offline", WillConfiguredAt: backend.Timestamp(time.Now())} packet := mqtt.Packet(mqtt.Topic(cfg, "status"), 1, false, status) client := mqtt.New(cfg, store(cfg)) if err := client.Start(ctx, []string{"commands"}, &paho.WillMessage{Topic: packet.Topic, Payload: packet.Payload, QoS: 1, Retain: false}); err != nil { t.Fatal(err) } defer client.Close() proxy.pause() deadline := time.NewTimer(5 * time.Second) defer deadline.Stop() for { select { case p := <-observer.Incoming: if strings.Contains(string(p.Payload), boot) { if !strings.Contains(string(p.Payload), "will_configured_at") { t.Fatal("Will invents disconnect timestamp") } t.Log("broker emits non-retained Will after actual abrupt TLS connection loss: PASS") return } case <-deadline.C: t.Fatal("Will not observed after abrupt connection loss") } } }