Files
meshcore-analyzer/cmd/ingestor/decoder_test.go

1509 lines
44 KiB
Go

package main
import (
"crypto/aes"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"math"
"strings"
"testing"
)
func TestDecodeHeaderRoutTypes(t *testing.T) {
tests := []struct {
b byte
rt int
name string
}{
{0x00, 0, "TRANSPORT_FLOOD"},
{0x01, 1, "FLOOD"},
{0x02, 2, "DIRECT"},
{0x03, 3, "TRANSPORT_DIRECT"},
}
for _, tt := range tests {
h := decodeHeader(tt.b)
if h.RouteType != tt.rt {
t.Errorf("header 0x%02X: routeType=%d, want %d", tt.b, h.RouteType, tt.rt)
}
if h.RouteTypeName != tt.name {
t.Errorf("header 0x%02X: routeTypeName=%s, want %s", tt.b, h.RouteTypeName, tt.name)
}
}
}
func TestDecodeHeaderPayloadTypes(t *testing.T) {
// 0x11 = 0b00_0100_01 → routeType=1(FLOOD), payloadType=4(ADVERT), version=0
h := decodeHeader(0x11)
if h.RouteType != 1 {
t.Errorf("0x11: routeType=%d, want 1", h.RouteType)
}
if h.PayloadType != 4 {
t.Errorf("0x11: payloadType=%d, want 4", h.PayloadType)
}
if h.PayloadVersion != 0 {
t.Errorf("0x11: payloadVersion=%d, want 0", h.PayloadVersion)
}
if h.RouteTypeName != "FLOOD" {
t.Errorf("0x11: routeTypeName=%s, want FLOOD", h.RouteTypeName)
}
if h.PayloadTypeName != "ADVERT" {
t.Errorf("0x11: payloadTypeName=%s, want ADVERT", h.PayloadTypeName)
}
}
func TestDecodePathZeroHops(t *testing.T) {
// 0x00: 0 hops, 1-byte hashes
pkt, err := DecodePacket("0500"+strings.Repeat("00", 10), nil)
if err != nil {
t.Fatal(err)
}
if pkt.Path.HashCount != 0 {
t.Errorf("hashCount=%d, want 0", pkt.Path.HashCount)
}
if pkt.Path.HashSize != 1 {
t.Errorf("hashSize=%d, want 1", pkt.Path.HashSize)
}
if len(pkt.Path.Hops) != 0 {
t.Errorf("hops=%d, want 0", len(pkt.Path.Hops))
}
}
func TestDecodePath1ByteHashes(t *testing.T) {
// 0x05: 5 hops, 1-byte hashes → 5 path bytes
pkt, err := DecodePacket("0505"+"AABBCCDDEE"+strings.Repeat("00", 10), nil)
if err != nil {
t.Fatal(err)
}
if pkt.Path.HashCount != 5 {
t.Errorf("hashCount=%d, want 5", pkt.Path.HashCount)
}
if pkt.Path.HashSize != 1 {
t.Errorf("hashSize=%d, want 1", pkt.Path.HashSize)
}
if len(pkt.Path.Hops) != 5 {
t.Fatalf("hops=%d, want 5", len(pkt.Path.Hops))
}
if pkt.Path.Hops[0] != "AA" {
t.Errorf("hop[0]=%s, want AA", pkt.Path.Hops[0])
}
if pkt.Path.Hops[4] != "EE" {
t.Errorf("hop[4]=%s, want EE", pkt.Path.Hops[4])
}
}
func TestDecodePath2ByteHashes(t *testing.T) {
// 0x45: 5 hops, 2-byte hashes
pkt, err := DecodePacket("0545"+"AA11BB22CC33DD44EE55"+strings.Repeat("00", 10), nil)
if err != nil {
t.Fatal(err)
}
if pkt.Path.HashCount != 5 {
t.Errorf("hashCount=%d, want 5", pkt.Path.HashCount)
}
if pkt.Path.HashSize != 2 {
t.Errorf("hashSize=%d, want 2", pkt.Path.HashSize)
}
if pkt.Path.Hops[0] != "AA11" {
t.Errorf("hop[0]=%s, want AA11", pkt.Path.Hops[0])
}
}
func TestDecodePath3ByteHashes(t *testing.T) {
// 0x8A: 10 hops, 3-byte hashes
pkt, err := DecodePacket("058A"+strings.Repeat("AA11FF", 10)+strings.Repeat("00", 10), nil)
if err != nil {
t.Fatal(err)
}
if pkt.Path.HashCount != 10 {
t.Errorf("hashCount=%d, want 10", pkt.Path.HashCount)
}
if pkt.Path.HashSize != 3 {
t.Errorf("hashSize=%d, want 3", pkt.Path.HashSize)
}
if len(pkt.Path.Hops) != 10 {
t.Errorf("hops=%d, want 10", len(pkt.Path.Hops))
}
}
func TestTransportCodes(t *testing.T) {
// Route type 0 (TRANSPORT_FLOOD) should have transport codes
hex := "1400" + "AABB" + "CCDD" + "1A" + strings.Repeat("00", 10)
pkt, err := DecodePacket(hex, nil)
if err != nil {
t.Fatal(err)
}
if pkt.Header.RouteType != 0 {
t.Errorf("routeType=%d, want 0", pkt.Header.RouteType)
}
if pkt.TransportCodes == nil {
t.Fatal("transportCodes should not be nil for TRANSPORT_FLOOD")
}
if pkt.TransportCodes.NextHop != "AABB" {
t.Errorf("nextHop=%s, want AABB", pkt.TransportCodes.NextHop)
}
if pkt.TransportCodes.LastHop != "CCDD" {
t.Errorf("lastHop=%s, want CCDD", pkt.TransportCodes.LastHop)
}
// Route type 1 (FLOOD) should NOT have transport codes
pkt2, err := DecodePacket("0500"+strings.Repeat("00", 10), nil)
if err != nil {
t.Fatal(err)
}
if pkt2.TransportCodes != nil {
t.Error("FLOOD should not have transport codes")
}
}
func TestDecodeAdvertFull(t *testing.T) {
pubkey := strings.Repeat("AA", 32)
timestamp := "78563412" // 0x12345678 LE
signature := strings.Repeat("BB", 64)
// flags: 0x92 = repeater(2) | hasLocation(0x10) | hasName(0x80)
flags := "92"
lat := "40933402" // ~37.0
lon := "E0E6B8F8" // ~-122.1
name := "546573744E6F6465" // "TestNode"
hex := "1200" + pubkey + timestamp + signature + flags + lat + lon + name
pkt, err := DecodePacket(hex, nil)
if err != nil {
t.Fatal(err)
}
if pkt.Payload.Type != "ADVERT" {
t.Errorf("type=%s, want ADVERT", pkt.Payload.Type)
}
if pkt.Payload.PubKey != strings.ToLower(pubkey) {
t.Errorf("pubkey mismatch")
}
if pkt.Payload.Timestamp != 0x12345678 {
t.Errorf("timestamp=%d, want %d", pkt.Payload.Timestamp, 0x12345678)
}
if pkt.Payload.Flags == nil {
t.Fatal("flags should not be nil")
}
if pkt.Payload.Flags.Raw != 0x92 {
t.Errorf("flags.raw=%d, want 0x92", pkt.Payload.Flags.Raw)
}
if pkt.Payload.Flags.Type != 2 {
t.Errorf("flags.type=%d, want 2", pkt.Payload.Flags.Type)
}
if !pkt.Payload.Flags.Repeater {
t.Error("flags.repeater should be true")
}
if pkt.Payload.Flags.Room {
t.Error("flags.room should be false")
}
if !pkt.Payload.Flags.HasLocation {
t.Error("flags.hasLocation should be true")
}
if !pkt.Payload.Flags.HasName {
t.Error("flags.hasName should be true")
}
if pkt.Payload.Lat == nil {
t.Fatal("lat should not be nil")
}
if math.Abs(*pkt.Payload.Lat-37.0) > 0.001 {
t.Errorf("lat=%f, want ~37.0", *pkt.Payload.Lat)
}
if pkt.Payload.Lon == nil {
t.Fatal("lon should not be nil")
}
if math.Abs(*pkt.Payload.Lon-(-122.1)) > 0.001 {
t.Errorf("lon=%f, want ~-122.1", *pkt.Payload.Lon)
}
if pkt.Payload.Name != "TestNode" {
t.Errorf("name=%s, want TestNode", pkt.Payload.Name)
}
}
func TestDecodeAdvertTypeEnums(t *testing.T) {
makeAdvert := func(flagsByte byte) *DecodedPacket {
hex := "1200" + strings.Repeat("AA", 32) + "00000000" + strings.Repeat("BB", 64) +
strings.ToUpper(string([]byte{hexDigit(flagsByte>>4), hexDigit(flagsByte & 0x0f)}))
pkt, err := DecodePacket(hex, nil)
if err != nil {
t.Fatal(err)
}
return pkt
}
// type 1 = chat/companion
p1 := makeAdvert(0x01)
if p1.Payload.Flags.Type != 1 {
t.Errorf("type 1: flags.type=%d", p1.Payload.Flags.Type)
}
if !p1.Payload.Flags.Chat {
t.Error("type 1: chat should be true")
}
// type 2 = repeater
p2 := makeAdvert(0x02)
if !p2.Payload.Flags.Repeater {
t.Error("type 2: repeater should be true")
}
// type 3 = room
p3 := makeAdvert(0x03)
if !p3.Payload.Flags.Room {
t.Error("type 3: room should be true")
}
// type 4 = sensor
p4 := makeAdvert(0x04)
if !p4.Payload.Flags.Sensor {
t.Error("type 4: sensor should be true")
}
}
func hexDigit(v byte) byte {
v = v & 0x0f
if v < 10 {
return '0' + v
}
return 'a' + v - 10
}
func TestDecodeAdvertNoLocationNoName(t *testing.T) {
hex := "1200" + strings.Repeat("CC", 32) + "00000000" + strings.Repeat("DD", 64) + "02"
pkt, err := DecodePacket(hex, nil)
if err != nil {
t.Fatal(err)
}
if pkt.Payload.Flags.HasLocation {
t.Error("hasLocation should be false")
}
if pkt.Payload.Flags.HasName {
t.Error("hasName should be false")
}
if pkt.Payload.Lat != nil {
t.Error("lat should be nil")
}
if pkt.Payload.Name != "" {
t.Errorf("name should be empty, got %s", pkt.Payload.Name)
}
}
func TestGoldenFixtureTxtMsg(t *testing.T) {
pkt, err := DecodePacket("0A00D69FD7A5A7475DB07337749AE61FA53A4788E976", nil)
if err != nil {
t.Fatal(err)
}
if pkt.Header.PayloadType != PayloadTXT_MSG {
t.Errorf("payloadType=%d, want %d", pkt.Header.PayloadType, PayloadTXT_MSG)
}
if pkt.Header.RouteType != RouteDirect {
t.Errorf("routeType=%d, want %d", pkt.Header.RouteType, RouteDirect)
}
if pkt.Path.HashCount != 0 {
t.Errorf("hashCount=%d, want 0", pkt.Path.HashCount)
}
if pkt.Payload.DestHash != "d6" {
t.Errorf("destHash=%s, want d6", pkt.Payload.DestHash)
}
if pkt.Payload.SrcHash != "9f" {
t.Errorf("srcHash=%s, want 9f", pkt.Payload.SrcHash)
}
}
func TestGoldenFixtureAdvert(t *testing.T) {
rawHex := "120046D62DE27D4C5194D7821FC5A34A45565DCC2537B300B9AB6275255CEFB65D840CE5C169C94C9AED39E8BCB6CB6EB0335497A198B33A1A610CD3B03D8DCFC160900E5244280323EE0B44CACAB8F02B5B38B91CFA18BD067B0B5E63E94CFC85F758A8530B9240933402E0E6B8F84D5252322D52"
pkt, err := DecodePacket(rawHex, nil)
if err != nil {
t.Fatal(err)
}
if pkt.Payload.Type != "ADVERT" {
t.Errorf("type=%s, want ADVERT", pkt.Payload.Type)
}
if pkt.Payload.PubKey != "46d62de27d4c5194d7821fc5a34a45565dcc2537b300b9ab6275255cefb65d84" {
t.Errorf("pubKey mismatch: %s", pkt.Payload.PubKey)
}
if pkt.Payload.Flags == nil || !pkt.Payload.Flags.Repeater {
t.Error("should be repeater")
}
if math.Abs(*pkt.Payload.Lat-37.0) > 0.001 {
t.Errorf("lat=%f, want ~37.0", *pkt.Payload.Lat)
}
if pkt.Payload.Name != "MRR2-R" {
t.Errorf("name=%s, want MRR2-R", pkt.Payload.Name)
}
}
func TestGoldenFixtureUnicodeAdvert(t *testing.T) {
rawHex := "120073CFF971E1CB5754A742C152B2D2E0EB108A19B246D663ED8898A72C4A5AD86EA6768E66694B025EDF6939D5C44CFF719C5D5520E5F06B20680A83AD9C2C61C3227BBB977A85EE462F3553445FECF8EDD05C234ECE217272E503F14D6DF2B1B9B133890C923CDF3002F8FDC1F85045414BF09F8CB3"
pkt, err := DecodePacket(rawHex, nil)
if err != nil {
t.Fatal(err)
}
if pkt.Payload.Type != "ADVERT" {
t.Errorf("type=%s, want ADVERT", pkt.Payload.Type)
}
if !pkt.Payload.Flags.Repeater {
t.Error("should be repeater")
}
// Name contains emoji: PEAK🌳
if !strings.HasPrefix(pkt.Payload.Name, "PEAK") {
t.Errorf("name=%s, expected to start with PEAK", pkt.Payload.Name)
}
}
func TestDecodePacketTooShort(t *testing.T) {
_, err := DecodePacket("FF", nil)
if err == nil {
t.Error("expected error for 1-byte packet")
}
}
func TestDecodePacketInvalidHex(t *testing.T) {
_, err := DecodePacket("ZZZZ", nil)
if err == nil {
t.Error("expected error for invalid hex")
}
}
func TestComputeContentHash(t *testing.T) {
hash := ComputeContentHash("0A00D69FD7A5A7475DB07337749AE61FA53A4788E976")
if len(hash) != 16 {
t.Errorf("hash length=%d, want 16", len(hash))
}
// Same content with different path should produce same hash
// (path bytes are stripped, only header + payload hashed)
// Verify consistency
hash2 := ComputeContentHash("0A00D69FD7A5A7475DB07337749AE61FA53A4788E976")
if hash != hash2 {
t.Error("content hash not deterministic")
}
}
func TestValidateAdvert(t *testing.T) {
goodPk := strings.Repeat("aa", 32)
// Good advert
good := &Payload{PubKey: goodPk, Flags: &AdvertFlags{Repeater: true}}
ok, _ := ValidateAdvert(good)
if !ok {
t.Error("good advert should validate")
}
// Nil
ok, _ = ValidateAdvert(nil)
if ok {
t.Error("nil should fail")
}
// Error payload
ok, _ = ValidateAdvert(&Payload{Error: "bad"})
if ok {
t.Error("error payload should fail")
}
// Short pubkey
ok, _ = ValidateAdvert(&Payload{PubKey: "aa"})
if ok {
t.Error("short pubkey should fail")
}
// All-zero pubkey
ok, _ = ValidateAdvert(&Payload{PubKey: strings.Repeat("0", 64)})
if ok {
t.Error("all-zero pubkey should fail")
}
// Invalid lat
badLat := 999.0
ok, _ = ValidateAdvert(&Payload{PubKey: goodPk, Lat: &badLat})
if ok {
t.Error("invalid lat should fail")
}
// Invalid lon
badLon := -999.0
ok, _ = ValidateAdvert(&Payload{PubKey: goodPk, Lon: &badLon})
if ok {
t.Error("invalid lon should fail")
}
// Control chars in name
ok, _ = ValidateAdvert(&Payload{PubKey: goodPk, Name: "test\x00name"})
if ok {
t.Error("control chars in name should fail")
}
// Name too long
ok, _ = ValidateAdvert(&Payload{PubKey: goodPk, Name: strings.Repeat("x", 65)})
if ok {
t.Error("long name should fail")
}
}
func TestDecodeGrpTxtShort(t *testing.T) {
p := decodeGrpTxt([]byte{0x01, 0x02}, nil)
if p.Error != "too short" {
t.Errorf("expected 'too short' error, got %q", p.Error)
}
if p.Type != "GRP_TXT" {
t.Errorf("type=%s, want GRP_TXT", p.Type)
}
}
func TestDecodeGrpTxtValid(t *testing.T) {
p := decodeGrpTxt([]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE}, nil)
if p.Error != "" {
t.Errorf("unexpected error: %s", p.Error)
}
if p.ChannelHash != 0xAA {
t.Errorf("channelHash=%d, want 0xAA", p.ChannelHash)
}
if p.MAC != "bbcc" {
t.Errorf("mac=%s, want bbcc", p.MAC)
}
if p.EncryptedData != "ddee" {
t.Errorf("encryptedData=%s, want ddee", p.EncryptedData)
}
}
func TestDecodeAnonReqShort(t *testing.T) {
p := decodeAnonReq(make([]byte, 10))
if p.Error != "too short" {
t.Errorf("expected 'too short' error, got %q", p.Error)
}
if p.Type != "ANON_REQ" {
t.Errorf("type=%s, want ANON_REQ", p.Type)
}
}
func TestDecodeAnonReqValid(t *testing.T) {
buf := make([]byte, 40)
buf[0] = 0xFF // destHash
for i := 1; i < 33; i++ {
buf[i] = byte(i)
}
buf[33] = 0xAA
buf[34] = 0xBB
p := decodeAnonReq(buf)
if p.Error != "" {
t.Errorf("unexpected error: %s", p.Error)
}
if p.DestHash != "ff" {
t.Errorf("destHash=%s, want ff", p.DestHash)
}
if p.MAC != "aabb" {
t.Errorf("mac=%s, want aabb", p.MAC)
}
}
func TestDecodePathPayloadShort(t *testing.T) {
p := decodePathPayload([]byte{0x01, 0x02, 0x03})
if p.Error != "too short" {
t.Errorf("expected 'too short' error, got %q", p.Error)
}
if p.Type != "PATH" {
t.Errorf("type=%s, want PATH", p.Type)
}
}
func TestDecodePathPayloadValid(t *testing.T) {
buf := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
p := decodePathPayload(buf)
if p.Error != "" {
t.Errorf("unexpected error: %s", p.Error)
}
if p.DestHash != "aa" {
t.Errorf("destHash=%s, want aa", p.DestHash)
}
if p.SrcHash != "bb" {
t.Errorf("srcHash=%s, want bb", p.SrcHash)
}
if p.PathData != "eeff" {
t.Errorf("pathData=%s, want eeff", p.PathData)
}
}
func TestDecodeTraceShort(t *testing.T) {
p := decodeTrace(make([]byte, 5))
if p.Error != "too short" {
t.Errorf("expected 'too short' error, got %q", p.Error)
}
if p.Type != "TRACE" {
t.Errorf("type=%s, want TRACE", p.Type)
}
}
func TestDecodeTraceValid(t *testing.T) {
buf := make([]byte, 16)
buf[0] = 0x00
buf[1] = 0x01 // tag LE uint32 = 1
buf[5] = 0xAA // destHash start
buf[11] = 0xBB
p := decodeTrace(buf)
if p.Error != "" {
t.Errorf("unexpected error: %s", p.Error)
}
if p.Tag != 1 {
t.Errorf("tag=%d, want 1", p.Tag)
}
if p.Type != "TRACE" {
t.Errorf("type=%s, want TRACE", p.Type)
}
}
func TestDecodeAdvertShort(t *testing.T) {
p := decodeAdvert(make([]byte, 50))
if p.Error != "too short for advert" {
t.Errorf("expected 'too short for advert' error, got %q", p.Error)
}
}
func TestDecodeEncryptedPayloadShort(t *testing.T) {
p := decodeEncryptedPayload("REQ", []byte{0x01, 0x02})
if p.Error != "too short" {
t.Errorf("expected 'too short' error, got %q", p.Error)
}
if p.Type != "REQ" {
t.Errorf("type=%s, want REQ", p.Type)
}
}
func TestDecodeEncryptedPayloadValid(t *testing.T) {
buf := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
p := decodeEncryptedPayload("RESPONSE", buf)
if p.Error != "" {
t.Errorf("unexpected error: %s", p.Error)
}
if p.DestHash != "aa" {
t.Errorf("destHash=%s, want aa", p.DestHash)
}
if p.SrcHash != "bb" {
t.Errorf("srcHash=%s, want bb", p.SrcHash)
}
if p.MAC != "ccdd" {
t.Errorf("mac=%s, want ccdd", p.MAC)
}
if p.EncryptedData != "eeff" {
t.Errorf("encryptedData=%s, want eeff", p.EncryptedData)
}
}
func TestDecodePayloadGRPData(t *testing.T) {
buf := []byte{0x01, 0x02, 0x03}
p := decodePayload(PayloadGRP_DATA, buf, nil)
if p.Type != "UNKNOWN" {
t.Errorf("type=%s, want UNKNOWN", p.Type)
}
if p.RawHex != "010203" {
t.Errorf("rawHex=%s, want 010203", p.RawHex)
}
}
func TestDecodePayloadRAWCustom(t *testing.T) {
buf := []byte{0xFF, 0xFE}
p := decodePayload(PayloadRAW_CUSTOM, buf, nil)
if p.Type != "UNKNOWN" {
t.Errorf("type=%s, want UNKNOWN", p.Type)
}
}
func TestDecodePayloadAllTypes(t *testing.T) {
// REQ
p := decodePayload(PayloadREQ, make([]byte, 10), nil)
if p.Type != "REQ" {
t.Errorf("REQ: type=%s", p.Type)
}
// RESPONSE
p = decodePayload(PayloadRESPONSE, make([]byte, 10), nil)
if p.Type != "RESPONSE" {
t.Errorf("RESPONSE: type=%s", p.Type)
}
// TXT_MSG
p = decodePayload(PayloadTXT_MSG, make([]byte, 10), nil)
if p.Type != "TXT_MSG" {
t.Errorf("TXT_MSG: type=%s", p.Type)
}
// ACK
p = decodePayload(PayloadACK, make([]byte, 10), nil)
if p.Type != "ACK" {
t.Errorf("ACK: type=%s", p.Type)
}
// GRP_TXT
p = decodePayload(PayloadGRP_TXT, make([]byte, 10), nil)
if p.Type != "GRP_TXT" {
t.Errorf("GRP_TXT: type=%s", p.Type)
}
// ANON_REQ
p = decodePayload(PayloadANON_REQ, make([]byte, 40), nil)
if p.Type != "ANON_REQ" {
t.Errorf("ANON_REQ: type=%s", p.Type)
}
// PATH
p = decodePayload(PayloadPATH, make([]byte, 10), nil)
if p.Type != "PATH" {
t.Errorf("PATH: type=%s", p.Type)
}
// TRACE
p = decodePayload(PayloadTRACE, make([]byte, 20), nil)
if p.Type != "TRACE" {
t.Errorf("TRACE: type=%s", p.Type)
}
}
func TestPayloadJSON(t *testing.T) {
p := &Payload{Type: "TEST", Name: "hello"}
j := PayloadJSON(p)
if j == "" || j == "{}" {
t.Errorf("PayloadJSON returned empty: %s", j)
}
if !strings.Contains(j, `"type":"TEST"`) {
t.Errorf("PayloadJSON missing type: %s", j)
}
if !strings.Contains(j, `"name":"hello"`) {
t.Errorf("PayloadJSON missing name: %s", j)
}
}
func TestPayloadJSONNil(t *testing.T) {
// nil should not panic
j := PayloadJSON(nil)
if j != "null" && j != "{}" {
// json.Marshal(nil) returns "null"
t.Logf("PayloadJSON(nil) = %s", j)
}
}
func TestValidateAdvertNaNLat(t *testing.T) {
goodPk := strings.Repeat("aa", 32)
nanVal := math.NaN()
ok, reason := ValidateAdvert(&Payload{PubKey: goodPk, Lat: &nanVal})
if ok {
t.Error("NaN lat should fail")
}
if !strings.Contains(reason, "lat") {
t.Errorf("reason should mention lat: %s", reason)
}
}
func TestValidateAdvertInfLon(t *testing.T) {
goodPk := strings.Repeat("aa", 32)
infVal := math.Inf(1)
ok, reason := ValidateAdvert(&Payload{PubKey: goodPk, Lon: &infVal})
if ok {
t.Error("Inf lon should fail")
}
if !strings.Contains(reason, "lon") {
t.Errorf("reason should mention lon: %s", reason)
}
}
func TestValidateAdvertNegInfLat(t *testing.T) {
goodPk := strings.Repeat("aa", 32)
negInf := math.Inf(-1)
ok, _ := ValidateAdvert(&Payload{PubKey: goodPk, Lat: &negInf})
if ok {
t.Error("-Inf lat should fail")
}
}
func TestValidateAdvertNaNLon(t *testing.T) {
goodPk := strings.Repeat("aa", 32)
nan := math.NaN()
ok, _ := ValidateAdvert(&Payload{PubKey: goodPk, Lon: &nan})
if ok {
t.Error("NaN lon should fail")
}
}
func TestValidateAdvertControlChars(t *testing.T) {
goodPk := strings.Repeat("aa", 32)
tests := []struct {
name string
char string
}{
{"null", "\x00"},
{"bell", "\x07"},
{"backspace", "\x08"},
{"vtab", "\x0b"},
{"formfeed", "\x0c"},
{"shift out", "\x0e"},
{"unit sep", "\x1f"},
{"delete", "\x7f"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ok, _ := ValidateAdvert(&Payload{PubKey: goodPk, Name: "test" + tt.char + "name"})
if ok {
t.Errorf("control char %q in name should fail", tt.char)
}
})
}
}
func TestValidateAdvertAllowedCharsInName(t *testing.T) {
goodPk := strings.Repeat("aa", 32)
// Tab (\t = 0x09), newline (\n = 0x0a), carriage return (\r = 0x0d) are NOT blocked
ok, reason := ValidateAdvert(&Payload{PubKey: goodPk, Name: "hello\tworld", Flags: &AdvertFlags{Repeater: true}})
if !ok {
t.Errorf("tab in name should be allowed, got reason: %s", reason)
}
}
func TestValidateAdvertUnknownRole(t *testing.T) {
goodPk := strings.Repeat("aa", 32)
// type=0 maps to companion via Chat=false, Repeater=false, Room=false, Sensor=false → companion
// type=5 (unknown) → companion (default), which IS a valid role
// But if all booleans are false AND type is 0, advertRole returns "companion" which is valid
// To get "unknown", we'd need a flags combo that doesn't match any valid role
// Actually advertRole always returns companion as default — so let's just test the validation path
flags := &AdvertFlags{Type: 5, Chat: false, Repeater: false, Room: false, Sensor: false}
ok, reason := ValidateAdvert(&Payload{PubKey: goodPk, Flags: flags})
// advertRole returns "companion" for this, which is valid
if !ok {
t.Errorf("default companion role should be valid, got: %s", reason)
}
}
func TestValidateAdvertValidLocation(t *testing.T) {
goodPk := strings.Repeat("aa", 32)
lat := 45.0
lon := -90.0
ok, _ := ValidateAdvert(&Payload{PubKey: goodPk, Lat: &lat, Lon: &lon, Flags: &AdvertFlags{Repeater: true}})
if !ok {
t.Error("valid lat/lon should pass")
}
}
func TestValidateAdvertBoundaryLat(t *testing.T) {
goodPk := strings.Repeat("aa", 32)
// Exactly at boundary
lat90 := 90.0
ok, _ := ValidateAdvert(&Payload{PubKey: goodPk, Lat: &lat90})
if !ok {
t.Error("lat=90 should pass")
}
latNeg90 := -90.0
ok, _ = ValidateAdvert(&Payload{PubKey: goodPk, Lat: &latNeg90})
if !ok {
t.Error("lat=-90 should pass")
}
// Just over
lat91 := 90.001
ok, _ = ValidateAdvert(&Payload{PubKey: goodPk, Lat: &lat91})
if ok {
t.Error("lat=90.001 should fail")
}
}
func TestValidateAdvertBoundaryLon(t *testing.T) {
goodPk := strings.Repeat("aa", 32)
lon180 := 180.0
ok, _ := ValidateAdvert(&Payload{PubKey: goodPk, Lon: &lon180})
if !ok {
t.Error("lon=180 should pass")
}
lonNeg180 := -180.0
ok, _ = ValidateAdvert(&Payload{PubKey: goodPk, Lon: &lonNeg180})
if !ok {
t.Error("lon=-180 should pass")
}
}
func TestComputeContentHashShortHex(t *testing.T) {
// Less than 16 hex chars and invalid hex
hash := ComputeContentHash("AB")
if hash != "AB" {
t.Errorf("short hex hash=%s, want AB", hash)
}
// Exactly 16 chars invalid hex
hash = ComputeContentHash("ZZZZZZZZZZZZZZZZ")
if len(hash) != 16 {
t.Errorf("invalid hex hash length=%d, want 16", len(hash))
}
}
func TestComputeContentHashTransportRoute(t *testing.T) {
// Route type 0 (TRANSPORT_FLOOD) with no path hops + 4 transport code bytes
// header=0x14 (TRANSPORT_FLOOD, ADVERT), path=0x00 (0 hops)
// transport codes = 4 bytes, then payload
hex := "1400" + "AABBCCDD" + strings.Repeat("EE", 10)
hash := ComputeContentHash(hex)
if len(hash) != 16 {
t.Errorf("hash length=%d, want 16", len(hash))
}
}
func TestComputeContentHashPayloadBeyondBuffer(t *testing.T) {
// path claims more bytes than buffer has → fallback
// header=0x05 (FLOOD, REQ), pathByte=0x3F (63 hops of 1 byte = 63 path bytes)
// but total buffer is only 4 bytes
hex := "053F" + "AABB"
hash := ComputeContentHash(hex)
// payloadStart = 2 + 63 = 65, but buffer is only 4 bytes
// Should fallback — rawHex is 8 chars (< 16), so returns rawHex
if hash != hex {
t.Errorf("hash=%s, want %s", hash, hex)
}
}
func TestComputeContentHashPayloadBeyondBufferLongHex(t *testing.T) {
// Same as above but with rawHex >= 16 chars → returns first 16
hex := "053F" + strings.Repeat("AA", 20) // 44 chars total, but pathByte claims 63 hops
hash := ComputeContentHash(hex)
if len(hash) != 16 {
t.Errorf("hash length=%d, want 16", len(hash))
}
if hash != hex[:16] {
t.Errorf("hash=%s, want %s", hash, hex[:16])
}
}
func TestComputeContentHashTransportBeyondBuffer(t *testing.T) {
// Transport route (0x00 = TRANSPORT_FLOOD) with path claiming some bytes
// total buffer too short for transport codes + path
// header=0x00, pathByte=0x02 (2 hops, 1-byte hash), then only 2 more bytes
// payloadStart = 2 + 2 + 4(transport) = 8, but buffer only 6 bytes
hex := "0002" + "AABB" + strings.Repeat("CC", 6) // 20 chars = 10 bytes
hash := ComputeContentHash(hex)
// payloadStart = 2 + 2 + 4 = 8, buffer is 10 bytes → should work
if len(hash) != 16 {
t.Errorf("hash length=%d, want 16", len(hash))
}
}
func TestComputeContentHashLongFallback(t *testing.T) {
// Long rawHex (>= 16) but invalid → returns first 16 chars
longInvalid := "ZZZZZZZZZZZZZZZZZZZZZZZZ"
hash := ComputeContentHash(longInvalid)
if hash != longInvalid[:16] {
t.Errorf("hash=%s, want first 16 of input", hash)
}
}
func TestDecodePacketWithWhitespace(t *testing.T) {
raw := "0A 00 D6 9F D7 A5 A7 47 5D B0 73 37 74 9A E6 1F A5 3A 47 88 E9 76"
pkt, err := DecodePacket(raw, nil)
if err != nil {
t.Fatal(err)
}
if pkt.Header.PayloadType != PayloadTXT_MSG {
t.Errorf("payloadType=%d, want %d", pkt.Header.PayloadType, PayloadTXT_MSG)
}
}
func TestDecodePacketWithNewlines(t *testing.T) {
raw := "0A00\nD69F\r\nD7A5A7475DB07337749AE61FA53A4788E976"
pkt, err := DecodePacket(raw, nil)
if err != nil {
t.Fatal(err)
}
if pkt.Payload.Type != "TXT_MSG" {
t.Errorf("type=%s, want TXT_MSG", pkt.Payload.Type)
}
}
func TestDecodePacketTransportRouteTooShort(t *testing.T) {
// TRANSPORT_FLOOD (route=0) but only 3 bytes total → too short for transport codes
_, err := DecodePacket("140011", nil)
if err == nil {
t.Error("expected error for transport route with too-short buffer")
}
if !strings.Contains(err.Error(), "transport codes") {
t.Errorf("error should mention transport codes: %v", err)
}
}
func TestDecodeAckShort(t *testing.T) {
p := decodeAck([]byte{0x01, 0x02, 0x03})
if p.Error != "too short" {
t.Errorf("expected 'too short', got %q", p.Error)
}
}
func TestDecodeAckValid(t *testing.T) {
buf := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
p := decodeAck(buf)
if p.Error != "" {
t.Errorf("unexpected error: %s", p.Error)
}
if p.DestHash != "aa" {
t.Errorf("destHash=%s, want aa", p.DestHash)
}
if p.ExtraHash != "ccddeeff" {
t.Errorf("extraHash=%s, want ccddeeff", p.ExtraHash)
}
}
func TestIsTransportRoute(t *testing.T) {
if !isTransportRoute(RouteTransportFlood) {
t.Error("RouteTransportFlood should be transport")
}
if !isTransportRoute(RouteTransportDirect) {
t.Error("RouteTransportDirect should be transport")
}
if isTransportRoute(RouteFlood) {
t.Error("RouteFlood should not be transport")
}
if isTransportRoute(RouteDirect) {
t.Error("RouteDirect should not be transport")
}
}
func TestDecodeHeaderUnknownTypes(t *testing.T) {
// Payload type that doesn't map to any known name
// bits 5-2 = 0x0C (12) is CONTROL but 0x0D (13) would be unknown
// byte = 0b00_1101_01 = 0x35 → routeType=1, payloadType=0x0D(13), version=0
h := decodeHeader(0x35)
if h.PayloadTypeName != "UNKNOWN" {
t.Errorf("payloadTypeName=%s, want UNKNOWN for type 13", h.PayloadTypeName)
}
}
func TestDecodePayloadMultipart(t *testing.T) {
// MULTIPART (0x0A) falls through to default → UNKNOWN
p := decodePayload(PayloadMULTIPART, []byte{0x01, 0x02}, nil)
if p.Type != "UNKNOWN" {
t.Errorf("MULTIPART type=%s, want UNKNOWN", p.Type)
}
}
func TestDecodePayloadControl(t *testing.T) {
// CONTROL (0x0B) falls through to default → UNKNOWN
p := decodePayload(PayloadCONTROL, []byte{0x01, 0x02}, nil)
if p.Type != "UNKNOWN" {
t.Errorf("CONTROL type=%s, want UNKNOWN", p.Type)
}
}
func TestDecodePathTruncatedBuffer(t *testing.T) {
// path byte claims 5 hops of 2 bytes = 10 bytes, but only 4 available
path, consumed := decodePath(0x45, []byte{0xAA, 0x11, 0xBB, 0x22}, 0)
if path.HashCount != 5 {
t.Errorf("hashCount=%d, want 5", path.HashCount)
}
// Should only decode 2 hops (4 bytes / 2 bytes per hop)
if len(path.Hops) != 2 {
t.Errorf("hops=%d, want 2 (truncated)", len(path.Hops))
}
if consumed != 10 {
t.Errorf("consumed=%d, want 10 (full claimed size)", consumed)
}
}
func TestDecodeFloodAdvert5Hops(t *testing.T) {
// From test-decoder.js Test 1
raw := "11451000D818206D3AAC152C8A91F89957E6D30CA51F36E28790228971C473B755F244F718754CF5EE4A2FD58D944466E42CDED140C66D0CC590183E32BAF40F112BE8F3F2BDF6012B4B2793C52F1D36F69EE054D9A05593286F78453E56C0EC4A3EB95DDA2A7543FCCC00B939CACC009278603902FC12BCF84B706120526F6F6620536F6C6172"
pkt, err := DecodePacket(raw, nil)
if err != nil {
t.Fatal(err)
}
if pkt.Header.RouteTypeName != "FLOOD" {
t.Errorf("route=%s, want FLOOD", pkt.Header.RouteTypeName)
}
if pkt.Header.PayloadTypeName != "ADVERT" {
t.Errorf("payload=%s, want ADVERT", pkt.Header.PayloadTypeName)
}
if pkt.Path.HashSize != 2 {
t.Errorf("hashSize=%d, want 2", pkt.Path.HashSize)
}
if pkt.Path.HashCount != 5 {
t.Errorf("hashCount=%d, want 5", pkt.Path.HashCount)
}
if pkt.Path.Hops[0] != "1000" {
t.Errorf("hop[0]=%s, want 1000", pkt.Path.Hops[0])
}
if pkt.Path.Hops[1] != "D818" {
t.Errorf("hop[1]=%s, want D818", pkt.Path.Hops[1])
}
if pkt.TransportCodes != nil {
t.Error("FLOOD should have no transport codes")
}
}
// --- Channel decryption tests ---
// buildTestCiphertext creates a valid AES-128-ECB encrypted GRP_TXT payload
// with a matching HMAC-SHA256 MAC for testing.
func buildTestCiphertext(channelKeyHex, senderMsg string, timestamp uint32) (ciphertextHex, macHex string) {
channelKey, _ := hex.DecodeString(channelKeyHex)
// Build plaintext: timestamp(4 LE) + flags(1) + message
plain := make([]byte, 4+1+len(senderMsg))
binary.LittleEndian.PutUint32(plain[0:4], timestamp)
plain[4] = 0x00 // flags
copy(plain[5:], senderMsg)
// Pad to AES block boundary
pad := aes.BlockSize - (len(plain) % aes.BlockSize)
if pad != aes.BlockSize {
plain = append(plain, make([]byte, pad)...)
}
// AES-128-ECB encrypt
block, _ := aes.NewCipher(channelKey)
ct := make([]byte, len(plain))
for i := 0; i < len(plain); i += aes.BlockSize {
block.Encrypt(ct[i:i+aes.BlockSize], plain[i:i+aes.BlockSize])
}
// HMAC-SHA256 MAC (first 2 bytes)
secret := make([]byte, 32)
copy(secret, channelKey)
h := hmac.New(sha256.New, secret)
h.Write(ct)
mac := h.Sum(nil)
return hex.EncodeToString(ct), hex.EncodeToString(mac[:2])
}
func TestDecryptChannelMessageValid(t *testing.T) {
key := "2cc3d22840e086105ad73443da2cacb8"
ctHex, macHex := buildTestCiphertext(key, "Alice: Hello world", 1700000000)
result, err := decryptChannelMessage(ctHex, macHex, key)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Sender != "Alice" {
t.Errorf("sender=%q, want Alice", result.Sender)
}
if result.Message != "Hello world" {
t.Errorf("message=%q, want 'Hello world'", result.Message)
}
if result.Timestamp != 1700000000 {
t.Errorf("timestamp=%d, want 1700000000", result.Timestamp)
}
}
func TestDecryptChannelMessageMACFail(t *testing.T) {
key := "2cc3d22840e086105ad73443da2cacb8"
ctHex, _ := buildTestCiphertext(key, "Alice: Hello", 100)
wrongMac := "ffff"
_, err := decryptChannelMessage(ctHex, wrongMac, key)
if err == nil {
t.Fatal("expected MAC verification failure")
}
if !strings.Contains(err.Error(), "MAC") {
t.Errorf("error should mention MAC: %v", err)
}
}
func TestDecryptChannelMessageWrongKey(t *testing.T) {
key := "2cc3d22840e086105ad73443da2cacb8"
ctHex, macHex := buildTestCiphertext(key, "Alice: Hello", 100)
wrongKey := "deadbeefdeadbeefdeadbeefdeadbeef"
_, err := decryptChannelMessage(ctHex, macHex, wrongKey)
if err == nil {
t.Fatal("expected error with wrong key")
}
}
func TestDecryptChannelMessageNoSender(t *testing.T) {
key := "aaaabbbbccccddddaaaabbbbccccdddd"
ctHex, macHex := buildTestCiphertext(key, "Just a message", 500)
result, err := decryptChannelMessage(ctHex, macHex, key)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Sender != "" {
t.Errorf("sender=%q, want empty", result.Sender)
}
if result.Message != "Just a message" {
t.Errorf("message=%q, want 'Just a message'", result.Message)
}
}
func TestDecryptChannelMessageSenderWithBrackets(t *testing.T) {
key := "aaaabbbbccccddddaaaabbbbccccdddd"
ctHex, macHex := buildTestCiphertext(key, "[admin]: Not a sender", 500)
result, err := decryptChannelMessage(ctHex, macHex, key)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Sender != "" {
t.Errorf("sender=%q, want empty (brackets disqualify)", result.Sender)
}
if result.Message != "[admin]: Not a sender" {
t.Errorf("message=%q", result.Message)
}
}
func TestDecryptChannelMessageInvalidKey(t *testing.T) {
_, err := decryptChannelMessage("aabb", "cc", "ZZZZ")
if err == nil {
t.Fatal("expected error for invalid key hex")
}
}
func TestDecryptChannelMessageShortKey(t *testing.T) {
_, err := decryptChannelMessage("aabb", "cc", "aabb")
if err == nil {
t.Fatal("expected error for short key")
}
}
func TestDecodeGrpTxtWithDecryption(t *testing.T) {
key := "2cc3d22840e086105ad73443da2cacb8"
ctHex, macHex := buildTestCiphertext(key, "Bob: Testing 123", 1700000000)
macBytes, _ := hex.DecodeString(macHex)
ctBytes, _ := hex.DecodeString(ctHex)
// Build GRP_TXT payload: channelHash(1) + MAC(2) + encrypted
buf := []byte{0xAA}
buf = append(buf, macBytes...)
buf = append(buf, ctBytes...)
keys := map[string]string{"#test": key}
p := decodeGrpTxt(buf, keys)
if p.Type != "CHAN" {
t.Errorf("type=%s, want CHAN", p.Type)
}
if p.DecryptionStatus != "decrypted" {
t.Errorf("decryptionStatus=%s, want decrypted", p.DecryptionStatus)
}
if p.Channel != "#test" {
t.Errorf("channel=%s, want #test", p.Channel)
}
if p.Sender != "Bob" {
t.Errorf("sender=%q, want Bob", p.Sender)
}
if p.Text != "Bob: Testing 123" {
t.Errorf("text=%q, want 'Bob: Testing 123'", p.Text)
}
if p.ChannelHash != 0xAA {
t.Errorf("channelHash=%d, want 0xAA", p.ChannelHash)
}
if p.ChannelHashHex != "AA" {
t.Errorf("channelHashHex=%s, want AA", p.ChannelHashHex)
}
if p.SenderTimestamp != 1700000000 {
t.Errorf("senderTimestamp=%d, want 1700000000", p.SenderTimestamp)
}
}
func TestDecodeGrpTxtDecryptionFailed(t *testing.T) {
key := "2cc3d22840e086105ad73443da2cacb8"
ctHex, macHex := buildTestCiphertext(key, "Hello", 100)
macBytes, _ := hex.DecodeString(macHex)
ctBytes, _ := hex.DecodeString(ctHex)
buf := []byte{0xFF}
buf = append(buf, macBytes...)
buf = append(buf, ctBytes...)
wrongKeys := map[string]string{"#wrong": "deadbeefdeadbeefdeadbeefdeadbeef"}
p := decodeGrpTxt(buf, wrongKeys)
if p.Type != "GRP_TXT" {
t.Errorf("type=%s, want GRP_TXT", p.Type)
}
if p.DecryptionStatus != "decryption_failed" {
t.Errorf("decryptionStatus=%s, want decryption_failed", p.DecryptionStatus)
}
if p.ChannelHashHex != "FF" {
t.Errorf("channelHashHex=%s, want FF", p.ChannelHashHex)
}
}
func TestDecodeGrpTxtNoKey(t *testing.T) {
buf := []byte{0x03, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22}
p := decodeGrpTxt(buf, nil)
if p.Type != "GRP_TXT" {
t.Errorf("type=%s, want GRP_TXT", p.Type)
}
if p.DecryptionStatus != "no_key" {
t.Errorf("decryptionStatus=%s, want no_key", p.DecryptionStatus)
}
if p.ChannelHashHex != "03" {
t.Errorf("channelHashHex=%s, want 03", p.ChannelHashHex)
}
}
func TestDecodeGrpTxtEmptyKeys(t *testing.T) {
buf := []byte{0xFF, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22}
p := decodeGrpTxt(buf, map[string]string{})
if p.DecryptionStatus != "no_key" {
t.Errorf("decryptionStatus=%s, want no_key", p.DecryptionStatus)
}
}
func TestDecodeGrpTxtShortEncryptedNoDecryptAttempt(t *testing.T) {
// encryptedData < 5 bytes (10 hex chars) → should not attempt decryption
buf := []byte{0xFF, 0xAA, 0xBB, 0xCC, 0xDD}
keys := map[string]string{"#test": "2cc3d22840e086105ad73443da2cacb8"}
p := decodeGrpTxt(buf, keys)
if p.DecryptionStatus != "no_key" {
t.Errorf("decryptionStatus=%s, want no_key (too short for decryption)", p.DecryptionStatus)
}
}
func TestDecodeGrpTxtMultipleKeysTriesAll(t *testing.T) {
correctKey := "2cc3d22840e086105ad73443da2cacb8"
ctHex, macHex := buildTestCiphertext(correctKey, "Eve: Found it", 999)
macBytes, _ := hex.DecodeString(macHex)
ctBytes, _ := hex.DecodeString(ctHex)
buf := []byte{0x01}
buf = append(buf, macBytes...)
buf = append(buf, ctBytes...)
keys := map[string]string{
"#wrong1": "deadbeefdeadbeefdeadbeefdeadbeef",
"#correct": correctKey,
"#wrong2": "11111111111111111111111111111111",
}
p := decodeGrpTxt(buf, keys)
if p.Type != "CHAN" {
t.Errorf("type=%s, want CHAN", p.Type)
}
if p.Channel != "#correct" {
t.Errorf("channel=%s, want #correct", p.Channel)
}
if p.Sender != "Eve" {
t.Errorf("sender=%q, want Eve", p.Sender)
}
}
func TestDecodeGrpTxtChannelHashHexZeroPad(t *testing.T) {
buf := []byte{0x03, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE}
p := decodeGrpTxt(buf, nil)
if p.ChannelHashHex != "03" {
t.Errorf("channelHashHex=%s, want 03 (zero-padded)", p.ChannelHashHex)
}
}
func TestDecodeGrpTxtChannelHashHexFF(t *testing.T) {
buf := []byte{0xFF, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE}
p := decodeGrpTxt(buf, nil)
if p.ChannelHashHex != "FF" {
t.Errorf("channelHashHex=%s, want FF", p.ChannelHashHex)
}
}
// --- Garbage text detection (fixes #197) ---
func TestDecryptChannelMessageGarbageText(t *testing.T) {
// Build ciphertext with binary garbage as the message
key := "2cc3d22840e086105ad73443da2cacb8"
garbage := "\x01\x02\x03\x80\x81"
ctHex, macHex := buildTestCiphertext(key, garbage, 1700000000)
_, err := decryptChannelMessage(ctHex, macHex, key)
if err == nil {
t.Fatal("expected error for garbage text, got nil")
}
if !strings.Contains(err.Error(), "non-printable") {
t.Errorf("error should mention non-printable: %v", err)
}
}
func TestDecryptChannelMessageValidText(t *testing.T) {
key := "2cc3d22840e086105ad73443da2cacb8"
ctHex, macHex := buildTestCiphertext(key, "Alice: Hello\nworld", 1700000000)
result, err := decryptChannelMessage(ctHex, macHex, key)
if err != nil {
t.Fatalf("unexpected error for valid text: %v", err)
}
if result.Sender != "Alice" {
t.Errorf("sender=%q, want Alice", result.Sender)
}
if result.Message != "Hello\nworld" {
t.Errorf("message=%q, want 'Hello\\nworld'", result.Message)
}
}
func TestDecodeGrpTxtGarbageMarkedFailed(t *testing.T) {
key := "2cc3d22840e086105ad73443da2cacb8"
garbage := "\x01\x02\x03\x04\x05"
ctHex, macHex := buildTestCiphertext(key, garbage, 1700000000)
macBytes, _ := hex.DecodeString(macHex)
ctBytes, _ := hex.DecodeString(ctHex)
buf := make([]byte, 1+2+len(ctBytes))
buf[0] = 0xFF // channel hash
buf[1] = macBytes[0]
buf[2] = macBytes[1]
copy(buf[3:], ctBytes)
keys := map[string]string{"#general": key}
p := decodeGrpTxt(buf, keys)
if p.DecryptionStatus != "decryption_failed" {
t.Errorf("decryptionStatus=%s, want decryption_failed", p.DecryptionStatus)
}
if p.Type != "GRP_TXT" {
t.Errorf("type=%s, want GRP_TXT", p.Type)
}
}
func TestDecodeAdvertWithTelemetry(t *testing.T) {
pubkey := strings.Repeat("AA", 32)
timestamp := "78563412"
signature := strings.Repeat("BB", 64)
flags := "94" // sensor(4) | hasLocation(0x10) | hasName(0x80)
lat := "40933402"
lon := "E0E6B8F8"
name := hex.EncodeToString([]byte("Sensor1"))
nullTerm := "00"
batteryLE := make([]byte, 2)
binary.LittleEndian.PutUint16(batteryLE, 3700)
tempLE := make([]byte, 2)
binary.LittleEndian.PutUint16(tempLE, uint16(int16(2850)))
hexStr := "1200" + pubkey + timestamp + signature + flags + lat + lon +
name + nullTerm +
hex.EncodeToString(batteryLE) + hex.EncodeToString(tempLE)
pkt, err := DecodePacket(hexStr, nil)
if err != nil {
t.Fatal(err)
}
if pkt.Payload.Name != "Sensor1" {
t.Errorf("name=%s, want Sensor1", pkt.Payload.Name)
}
if pkt.Payload.BatteryMv == nil {
t.Fatal("battery_mv should not be nil")
}
if *pkt.Payload.BatteryMv != 3700 {
t.Errorf("battery_mv=%d, want 3700", *pkt.Payload.BatteryMv)
}
if pkt.Payload.TemperatureC == nil {
t.Fatal("temperature_c should not be nil")
}
if math.Abs(*pkt.Payload.TemperatureC-28.50) > 0.01 {
t.Errorf("temperature_c=%f, want 28.50", *pkt.Payload.TemperatureC)
}
}
func TestDecodeAdvertWithTelemetryNegativeTemp(t *testing.T) {
pubkey := strings.Repeat("CC", 32)
timestamp := "00000000"
signature := strings.Repeat("DD", 64)
flags := "84" // sensor(4) | hasName(0x80), no location
name := hex.EncodeToString([]byte("Cold"))
nullTerm := "00"
batteryLE := make([]byte, 2)
binary.LittleEndian.PutUint16(batteryLE, 4200)
tempLE := make([]byte, 2)
var negTemp int16 = -550
binary.LittleEndian.PutUint16(tempLE, uint16(negTemp))
hexStr := "1200" + pubkey + timestamp + signature + flags +
name + nullTerm +
hex.EncodeToString(batteryLE) + hex.EncodeToString(tempLE)
pkt, err := DecodePacket(hexStr, nil)
if err != nil {
t.Fatal(err)
}
if pkt.Payload.Name != "Cold" {
t.Errorf("name=%s, want Cold", pkt.Payload.Name)
}
if pkt.Payload.BatteryMv == nil || *pkt.Payload.BatteryMv != 4200 {
t.Errorf("battery_mv=%v, want 4200", pkt.Payload.BatteryMv)
}
if pkt.Payload.TemperatureC == nil {
t.Fatal("temperature_c should not be nil")
}
if math.Abs(*pkt.Payload.TemperatureC-(-5.50)) > 0.01 {
t.Errorf("temperature_c=%f, want -5.50", *pkt.Payload.TemperatureC)
}
}
func TestDecodeAdvertWithoutTelemetry(t *testing.T) {
pubkey := strings.Repeat("EE", 32)
timestamp := "00000000"
signature := strings.Repeat("FF", 64)
flags := "82" // repeater(2) | hasName(0x80)
name := hex.EncodeToString([]byte("Node1"))
hexStr := "1200" + pubkey + timestamp + signature + flags + name
pkt, err := DecodePacket(hexStr, nil)
if err != nil {
t.Fatal(err)
}
if pkt.Payload.Name != "Node1" {
t.Errorf("name=%s, want Node1", pkt.Payload.Name)
}
if pkt.Payload.BatteryMv != nil {
t.Errorf("battery_mv should be nil for advert without telemetry, got %d", *pkt.Payload.BatteryMv)
}
if pkt.Payload.TemperatureC != nil {
t.Errorf("temperature_c should be nil for advert without telemetry, got %f", *pkt.Payload.TemperatureC)
}
}
func TestDecodeAdvertNonSensorIgnoresTelemetryBytes(t *testing.T) {
// A repeater node with 4 trailing bytes after the name should NOT decode telemetry.
pubkey := strings.Repeat("AB", 32)
timestamp := "00000000"
signature := strings.Repeat("CD", 64)
flags := "82" // repeater(2) | hasName(0x80)
name := hex.EncodeToString([]byte("Rptr"))
nullTerm := "00"
extraBytes := "B40ED403" // battery-like and temp-like bytes
hexStr := "1200" + pubkey + timestamp + signature + flags + name + nullTerm + extraBytes
pkt, err := DecodePacket(hexStr, nil)
if err != nil {
t.Fatal(err)
}
if pkt.Payload.BatteryMv != nil {
t.Errorf("battery_mv should be nil for non-sensor node, got %d", *pkt.Payload.BatteryMv)
}
if pkt.Payload.TemperatureC != nil {
t.Errorf("temperature_c should be nil for non-sensor node, got %f", *pkt.Payload.TemperatureC)
}
}
func TestDecodeAdvertTelemetryZeroTemp(t *testing.T) {
// 0°C is a valid temperature and must be emitted.
pubkey := strings.Repeat("12", 32)
timestamp := "00000000"
signature := strings.Repeat("34", 64)
flags := "84" // sensor(4) | hasName(0x80)
name := hex.EncodeToString([]byte("FreezeSensor"))
nullTerm := "00"
batteryLE := make([]byte, 2)
binary.LittleEndian.PutUint16(batteryLE, 3600)
tempLE := make([]byte, 2) // tempRaw=0 → 0°C
hexStr := "1200" + pubkey + timestamp + signature + flags +
name + nullTerm +
hex.EncodeToString(batteryLE) + hex.EncodeToString(tempLE)
pkt, err := DecodePacket(hexStr, nil)
if err != nil {
t.Fatal(err)
}
if pkt.Payload.TemperatureC == nil {
t.Fatal("temperature_c should not be nil for 0°C")
}
if *pkt.Payload.TemperatureC != 0.0 {
t.Errorf("temperature_c=%f, want 0.0", *pkt.Payload.TemperatureC)
}
}