Deploy MQTT test backend with private viewer and Gitea CI/CD
Test and deploy MQTT backend / test (push) Successful in 2m8s
Test and deploy MQTT backend / deploy (push) Successful in 18s

This commit is contained in:
2026-10-08 14:48:53 +03:30
commit 71ef622b4f
70 changed files with 7123 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
package backend
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"time"
"github.com/eclipse/paho.golang/paho"
"robot-market/test-mqtt-5/internal/events"
)
func TestReceiptRawPayloadRoutingAndCorrelation(t *testing.T) {
b := testBackend()
b.Client.Config.Topics.ObserveAll = true
for _, payload := range []string{"hello", "", "[1,2]", `{"device_id":"different","timestamp":"{{$isoTimestamp}}","message_id":"msg-1"}`} {
reply := b.receipt(&paho.Publish{Topic: "robot-market/test/v1/real-device/telemetry", Payload: []byte(payload)})
if reply == nil || reply.Topic != "robot-market/test/v1/real-device/acks" || reply.Retain || reply.QoS != 1 {
t.Fatal("missing/routed/retained reply")
}
var ack ReceiptAck
if json.Unmarshal(reply.Payload, &ack) != nil || !ack.OK || ack.Type != "receipt_ack" || ack.PayloadBytes != len(payload) || len(ack.PayloadSHA256) != 64 {
t.Fatal("invalid receipt")
}
}
p := &paho.Publish{Topic: "robot-market/test/v1/device/telemetry", Payload: []byte("x"), Properties: &paho.PublishProperties{ResponseTopic: "robot-market/test/v1/replies/custom", CorrelationData: []byte{0, 255, 1}}}
reply := b.receipt(p)
p.Properties.CorrelationData[0] = 99
if reply.Topic != "robot-market/test/v1/replies/custom" || reply.Properties.CorrelationData[0] != 0 || reply.Properties.ResponseTopic != "" {
t.Fatal("native response/correlation lost or aliased")
}
p.Properties.ResponseTopic = "outside/test"
if b.receipt(p).Topic != "robot-market/test/v1/device/acks" {
t.Fatal("unusable response topic should use default receipt topic")
}
}
func TestReceiptLoopAndRetainedReplayPrevention(t *testing.T) {
b := testBackend()
b.Client.Config.Topics.ObserveAll = true
for _, topic := range []string{"acks", "commands", "receipts"} {
if b.receipt(&paho.Publish{Topic: "robot-market/test/v1/device/" + topic, Payload: []byte("hello")}) != nil {
t.Fatal("ACK loop possible")
}
}
for _, p := range []*paho.Publish{
{Topic: "robot-market/test/v1/device/status", Retain: true, Payload: []byte("online")},
{Topic: "robot-market/test/v1/custom/topic", Payload: []byte(`{"type":"receipt_ack","ok":true}`)},
{Topic: "outside/device/topic", Payload: []byte("hello")},
} {
if b.receipt(p) != nil {
t.Fatal("reply created for replay, receipt or external topic")
}
}
}
func TestReceiptDuplicateDeliveryRepliesWithoutCommandExecution(t *testing.T) {
b := testBackend()
b.Client.Config.Topics.ObserveAll = true
b.Client.Config.Receipts.Enabled = true
p := &paho.Publish{Topic: "robot-market/test/v1/device/telemetry", Payload: []byte("same message")}
b.Handle(p)
b.Handle(p)
if len(b.receipts) != 2 || len(b.pending) != 0 {
t.Fatal("duplicate receipt handling must not execute commands")
}
}
func TestReceiptQueueFailureAndShutdown(t *testing.T) {
b := testBackend()
b.Client.Config.Topics.ObserveAll = true
b.Client.Config.Receipts.Enabled = true
b.receipts = make(chan *paho.Publish, 1)
p := &paho.Publish{Topic: "robot-market/test/v1/device/telemetry", Payload: []byte("hello")}
b.Handle(p)
b.Handle(p)
_, counts := b.Client.Events.Snapshot()
if counts[events.Kind("receipt_ack_dropped")] != 1 {
t.Fatal("queue overflow invisible")
}
b.PublishReceipt = func(context.Context, *paho.Publish) error { return errors.New("test disconnect") }
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { b.receiptWorker(ctx); close(done) }()
deadline := time.Now().Add(time.Second)
for {
_, counts = b.Client.Events.Snapshot()
if counts[events.Kind("receipt_ack_failed")] == 1 {
break
}
if time.Now().After(deadline) {
t.Fatal("failure not recorded")
}
time.Sleep(time.Millisecond)
}
cancel()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("worker shutdown blocked")
}
history, _ := b.Client.Events.Snapshot()
if !strings.Contains(history[len(history)-1].Outcome, "not confirmed") {
t.Fatal("failure claims success")
}
}