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
+326
View File
@@ -0,0 +1,326 @@
// Package contracts defines the isolated test payloads, not the existing device protocol.
package contracts
import (
"bytes"
"encoding/json"
"fmt"
"io"
"math"
"regexp"
"strings"
"time"
"unicode/utf8"
)
const MaxPayloadBytes = 16384
const MaxCommandLifetime = time.Minute
var safeID = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`)
type Envelope struct {
SchemaVersion int `json:"schema_version"`
DeviceID string `json:"device_id"`
}
type Telemetry struct {
Envelope
MessageID string `json:"message_id"`
BootID string `json:"boot_id"`
Sequence *uint64 `json:"sequence"`
Timestamp string `json:"timestamp"`
Readings map[string]float64 `json:"readings"`
}
type Status struct {
Envelope
BootID string `json:"boot_id"`
State string `json:"state"`
Timestamp string `json:"timestamp,omitempty"`
WillConfiguredAt string `json:"will_configured_at,omitempty"`
Implementation string `json:"implementation,omitempty"`
ReportedProtocol string `json:"reported_protocol,omitempty"`
}
type Command struct {
Envelope
RequestID string `json:"request_id"`
IssuedAt string `json:"issued_at"`
ExpiresAt string `json:"expires_at"`
Command string `json:"command"`
}
type Result struct {
Pong *bool `json:"pong,omitempty"`
Version string `json:"version,omitempty"`
}
type AckError struct {
Code string `json:"code"`
Message string `json:"message"`
}
type Ack struct {
Envelope
RequestID string `json:"request_id"`
Timestamp string `json:"timestamp"`
Stage string `json:"stage"`
OK *bool `json:"ok"`
Result *Result `json:"result,omitempty"`
Error *AckError `json:"error,omitempty"`
}
func (e Envelope) validate(device string) error {
if e.SchemaVersion != 1 {
return fmt.Errorf("schema_version: expected 1")
}
if !safeID.MatchString(e.DeviceID) || e.DeviceID != device {
return fmt.Errorf("device_id: invalid or mismatched topic identity")
}
return nil
}
func id(name, value string) error {
if !safeID.MatchString(value) {
return fmt.Errorf("%s: expected 1..64 safe identifier characters", name)
}
return nil
}
func timestamp(name, value string) (time.Time, error) {
t, err := time.Parse(time.RFC3339Nano, value)
if err != nil || !strings.HasSuffix(value, "Z") {
return time.Time{}, fmt.Errorf("%s: expected RFC3339 UTC timestamp ending Z", name)
}
return t, nil
}
func (m Telemetry) Validate(device string) error {
if err := m.Envelope.validate(device); err != nil {
return err
}
if err := id("message_id", m.MessageID); err != nil {
return err
}
if err := id("boot_id", m.BootID); err != nil {
return err
}
if _, err := timestamp("timestamp", m.Timestamp); err != nil {
return err
}
if m.Sequence == nil {
return fmt.Errorf("sequence: required")
}
if len(m.Readings) == 0 || len(m.Readings) > 16 {
return fmt.Errorf("readings: expected 1..16 numeric readings")
}
for key, value := range m.Readings {
if !safeID.MatchString(key) || math.IsNaN(value) || math.IsInf(value, 0) {
return fmt.Errorf("readings: invalid name or nonfinite value")
}
}
return nil
}
func (m Status) Validate(device string) error {
if err := m.Envelope.validate(device); err != nil {
return err
}
if err := id("boot_id", m.BootID); err != nil {
return err
}
if m.State != "online" && m.State != "offline" {
return fmt.Errorf("state: expected online or offline")
}
if m.Timestamp != "" && m.WillConfiguredAt != "" {
return fmt.Errorf("timestamp: cannot be combined with will_configured_at")
}
if m.WillConfiguredAt != "" {
if m.State != "offline" {
return fmt.Errorf("will_configured_at: only valid for offline Will")
}
if _, err := timestamp("will_configured_at", m.WillConfiguredAt); err != nil {
return err
}
} else {
if _, err := timestamp("timestamp", m.Timestamp); err != nil {
return err
}
}
if m.Implementation != "" {
if err := id("implementation", m.Implementation); err != nil {
return err
}
}
if m.ReportedProtocol != "" && m.ReportedProtocol != "5" && m.ReportedProtocol != "3.1.1" {
return fmt.Errorf("reported_protocol: expected 5 or 3.1.1")
}
return nil
}
func (m Command) Validate(device string) error {
if err := m.Envelope.validate(device); err != nil {
return err
}
if err := id("request_id", m.RequestID); err != nil {
return err
}
if m.Command != "ping" && m.Command != "get_version" {
return fmt.Errorf("command: only ping or get_version allowed")
}
issued, err := timestamp("issued_at", m.IssuedAt)
if err != nil {
return err
}
expires, err := timestamp("expires_at", m.ExpiresAt)
if err != nil {
return err
}
lifetime := expires.Sub(issued)
if lifetime < time.Second || lifetime > MaxCommandLifetime {
return fmt.Errorf("expires_at: lifetime must be 1s..1m after issued_at")
}
return nil
}
// CheckDeadline is deliberately separate from structural validation, for delayed/replayed messages.
func (m Command) CheckDeadline(now time.Time) error {
if err := m.Validate(m.DeviceID); err != nil {
return err
}
issued, _ := timestamp("issued_at", m.IssuedAt)
expires, _ := timestamp("expires_at", m.ExpiresAt)
if now.Before(issued) {
return fmt.Errorf("issued_at: command is from the future; check device clock")
}
if !now.Before(expires) {
return fmt.Errorf("expires_at: command expired")
}
return nil
}
func (m Ack) Validate(device string) error {
if err := m.Envelope.validate(device); err != nil {
return err
}
if err := id("request_id", m.RequestID); err != nil {
return err
}
if _, err := timestamp("timestamp", m.Timestamp); err != nil {
return err
}
if m.OK == nil {
return fmt.Errorf("ok: boolean is required")
}
switch m.Stage {
case "accepted":
if !*m.OK || m.Result != nil || m.Error != nil {
return fmt.Errorf("accepted: requires ok=true without result/error")
}
case "completed":
if !*m.OK || m.Error != nil || m.Result == nil {
return fmt.Errorf("completed: requires ok=true and result without error")
}
if (m.Result.Pong != nil) == (m.Result.Version != "") {
return fmt.Errorf("result: expected exactly one of pong or version")
}
if m.Result.Pong != nil && !*m.Result.Pong {
return fmt.Errorf("result.pong: expected true")
}
if m.Result.Version != "" && !regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$`).MatchString(m.Result.Version) {
return fmt.Errorf("result.version: invalid or too long")
}
case "rejected", "failed":
if *m.OK || m.Error == nil || m.Result != nil {
return fmt.Errorf("rejected/failed: requires ok=false and error without result")
}
if err := id("error.code", m.Error.Code); err != nil {
return err
}
if m.Error.Message == "" || len(m.Error.Message) > 256 {
return fmt.Errorf("error.message: expected 1..256 bytes")
}
default:
return fmt.Errorf("stage: expected accepted, completed, rejected or failed")
}
return nil
}
// Decode rejects duplicate keys, unknown fields, invalid UTF-8 and trailing values.
// Errors contain no payload contents. The returned concrete type implements Validate.
func Decode(kind string, data []byte, device string) (any, error) {
if len(data) > MaxPayloadBytes || len(data) == 0 {
return nil, fmt.Errorf("payload: expected 1..16384 bytes")
}
if !utf8.Valid(data) {
return nil, fmt.Errorf("payload: invalid UTF-8")
}
if err := uniqueObject(data); err != nil {
return nil, err
}
var target interface{ Validate(string) error }
switch kind {
case "telemetry":
target = &Telemetry{}
case "status":
target = &Status{}
case "commands":
target = &Command{}
case "acks":
target = &Ack{}
default:
return nil, fmt.Errorf("topic: unsupported message category")
}
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err := dec.Decode(target); err != nil {
return nil, fmt.Errorf("payload: invalid JSON field or type")
}
if err := target.Validate(device); err != nil {
return nil, err
}
return target, nil
}
func uniqueObject(data []byte) error {
d := json.NewDecoder(bytes.NewReader(data))
d.UseNumber()
first, err := d.Token()
if err != nil || first != json.Delim('{') {
return fmt.Errorf("payload: expected JSON object")
}
if err = walkObject(d, 0); err != nil {
return fmt.Errorf("payload: malformed JSON, duplicate key or excessive nesting")
}
if _, err = d.Token(); err != io.EOF {
return fmt.Errorf("payload: trailing JSON data")
}
return nil
}
func walkObject(d *json.Decoder, depth int) error {
if depth > 8 {
return fmt.Errorf("depth")
}
seen := map[string]bool{}
for d.More() {
token, err := d.Token()
if err != nil {
return err
}
key, ok := token.(string)
// encoding/json matches struct fields case-insensitively. Reject aliases
// of an existing key as well as byte-identical duplicates.
canonical := strings.ToLower(key)
if !ok || seen[canonical] {
return fmt.Errorf("duplicate")
}
seen[canonical] = true
t, err := d.Token()
if err != nil {
return err
}
if t == nil {
return fmt.Errorf("null values unsupported")
}
if delim, ok := t.(json.Delim); ok {
if delim != '{' {
return fmt.Errorf("arrays unsupported")
}
if err = walkObject(d, depth+1); err != nil {
return err
}
}
}
end, err := d.Token()
if err != nil || end != json.Delim('}') {
return fmt.Errorf("object")
}
return nil
}