Files
meshcore-analyzer/cmd/ingestor/decoder_test.go
Kpa-clawbot e89c2bfe1f test: add comprehensive Go test coverage for ingestor (80%) and server (90%)
- ingestor: add config_test.go (LoadConfig, env overrides, legacy MQTT)
- ingestor: add main_test.go (toFloat64, firstNonEmpty, handleMessage, advertRole)
- ingestor: extend decoder_test.go (short buffer errors, edge cases, all payload types)
- ingestor: extend db_test.go (empty hash, timestamp updates, BuildPacketData, schema)
- server: add config_test.go (LoadConfig, LoadTheme, health thresholds, ResolveDBPath)
- server: add helpers_test.go (writeJSON/Error, queryInt, mergeMap, round, percentile, spaHandler)
- server: extend db_test.go (all query functions, filters, channel messages, node health)
- server: extend routes_test.go (all endpoints, error paths, analytics, observer analytics)
- server: extend websocket_test.go (multi-client, buffer full, poller cycle)

Coverage: ingestor 48% -> 80%, server 52% -> 90%

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-27 00:07:44 -07:00

1027 lines
29 KiB
Go

package main
import (
"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))
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))
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))
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))
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)
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))
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)
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)
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)
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")
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)
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)
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")
if err == nil {
t.Error("expected error for 1-byte packet")
}
}
func TestDecodePacketInvalidHex(t *testing.T) {
_, err := DecodePacket("ZZZZ")
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})
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})
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)
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)
if p.Type != "UNKNOWN" {
t.Errorf("type=%s, want UNKNOWN", p.Type)
}
}
func TestDecodePayloadAllTypes(t *testing.T) {
// REQ
p := decodePayload(PayloadREQ, make([]byte, 10))
if p.Type != "REQ" {
t.Errorf("REQ: type=%s", p.Type)
}
// RESPONSE
p = decodePayload(PayloadRESPONSE, make([]byte, 10))
if p.Type != "RESPONSE" {
t.Errorf("RESPONSE: type=%s", p.Type)
}
// TXT_MSG
p = decodePayload(PayloadTXT_MSG, make([]byte, 10))
if p.Type != "TXT_MSG" {
t.Errorf("TXT_MSG: type=%s", p.Type)
}
// ACK
p = decodePayload(PayloadACK, make([]byte, 10))
if p.Type != "ACK" {
t.Errorf("ACK: type=%s", p.Type)
}
// GRP_TXT
p = decodePayload(PayloadGRP_TXT, make([]byte, 10))
if p.Type != "GRP_TXT" {
t.Errorf("GRP_TXT: type=%s", p.Type)
}
// ANON_REQ
p = decodePayload(PayloadANON_REQ, make([]byte, 40))
if p.Type != "ANON_REQ" {
t.Errorf("ANON_REQ: type=%s", p.Type)
}
// PATH
p = decodePayload(PayloadPATH, make([]byte, 10))
if p.Type != "PATH" {
t.Errorf("PATH: type=%s", p.Type)
}
// TRACE
p = decodePayload(PayloadTRACE, make([]byte, 20))
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)
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)
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")
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})
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})
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)
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")
}
}