package envelope import ( "encoding/json" "testing" "time" ) func TestFrameEnvelopeVehicleKeyPrefersVIN(t *testing.T) { e := FrameEnvelope{Protocol: ProtocolJT808, VIN: "LNBVIN00000000001", Phone: "13307795425"} if got := e.VehicleKey(); got != "LNBVIN00000000001" { t.Fatalf("VehicleKey() = %q", got) } } func TestFrameEnvelopeVehicleKeyFallsBackToPhone(t *testing.T) { e := FrameEnvelope{Protocol: ProtocolJT808, Phone: "13307795425"} if got := e.VehicleKey(); got != "JT808:13307795425" { t.Fatalf("VehicleKey() = %q", got) } } func TestFrameEnvelopeEventIDStable(t *testing.T) { e := FrameEnvelope{ Protocol: ProtocolJT808, MessageID: "0x0200", Phone: "013307795425", Sequence: 1, EventTimeMS: 1782745114000, ReceivedAtMS: 1782745114999, RawHex: "7e02000000ff7e", } a := e.StableEventID() b := e.StableEventID() if a == "" || a != b { t.Fatalf("event id must be non-empty and stable: %q %q", a, b) } } func TestFrameEnvelopeMarshalDefaults(t *testing.T) { e := FrameEnvelope{Protocol: ProtocolGB32960, VIN: "LNBVIN00000000002", MessageID: "0x02"} payload, err := e.MarshalJSONBytes() if err != nil { t.Fatalf("MarshalJSONBytes() error = %v", err) } var decoded FrameEnvelope if err := json.Unmarshal(payload, &decoded); err != nil { t.Fatalf("json.Unmarshal() error = %v", err) } if decoded.EventID == "" { t.Fatal("event id should be filled") } if decoded.ParseStatus != ParseOK { t.Fatalf("parse status = %q", decoded.ParseStatus) } if decoded.EventKind != EventKindRaw { t.Fatalf("event kind = %q, want %q", decoded.EventKind, EventKindRaw) } } func TestNormalizedEventTimeMSFallsBackToReceivedWhenEventIsFarFuture(t *testing.T) { received := time.Date(2026, 7, 12, 9, 30, 0, 0, time.UTC).UnixMilli() event := time.Date(2026, 7, 13, 9, 30, 0, 0, time.UTC).UnixMilli() got, ok := NormalizedEventTimeMS(FrameEnvelope{EventTimeMS: event, ReceivedAtMS: received}) if !ok { t.Fatal("NormalizedEventTimeMS() ok = false") } if got != received { t.Fatalf("normalized event time = %d, want received %d", got, received) } _, reason, ok := NormalizedEventTimeMSWithReason(FrameEnvelope{EventTimeMS: event, ReceivedAtMS: received}) if !ok || reason != EventTimeReasonReceivedFutureEvent { t.Fatalf("reason = %q ok=%v, want future fallback", reason, ok) } } func TestNormalizedEventTimeMSKeepsSmallFutureSkew(t *testing.T) { received := time.Date(2026, 7, 12, 9, 30, 0, 0, time.UTC).UnixMilli() event := time.Date(2026, 7, 12, 9, 35, 0, 0, time.UTC).UnixMilli() got, ok := NormalizedEventTimeMS(FrameEnvelope{EventTimeMS: event, ReceivedAtMS: received}) if !ok { t.Fatal("NormalizedEventTimeMS() ok = false") } if got != event { t.Fatalf("normalized event time = %d, want event %d", got, event) } } func TestIsRealtimeTelemetryFrameRejectsKnownNonRealtimeMessageIDs(t *testing.T) { fields := map[string]any{ FieldLatitude: 30.590151, FieldLongitude: 121.069881, FieldTotalMileageKM: 10241.2, } tests := []FrameEnvelope{ {Protocol: ProtocolJT808, MessageID: "0x0100", Fields: fields, ParseStatus: ParseOK}, {Protocol: ProtocolGB32960, MessageID: "0x01", Fields: fields, Parsed: map[string]any{"data_units": []any{}}, ParseStatus: ParseOK}, } for _, env := range tests { if IsRealtimeTelemetryFrame(env) { t.Fatalf("IsRealtimeTelemetryFrame(%s/%s) = true, want false", env.Protocol, env.MessageID) } } } func TestIsRealtimeTelemetryFrameAcceptsKnownRealtimeMessageIDs(t *testing.T) { tests := []FrameEnvelope{ {Protocol: ProtocolJT808, MessageID: "0x0200", ParsedFields: map[string]any{"jt808.location.speed_kmh": 1}, ParseStatus: ParseOK}, {Protocol: ProtocolGB32960, MessageID: "0x02", ParsedFields: map[string]any{"gb32960.vehicle.speed_kmh": 1}, ParseStatus: ParseOK}, {Protocol: ProtocolGB32960, MessageID: "0x03", ParsedFields: map[string]any{"gb32960.vehicle.speed_kmh": 1}, ParseStatus: ParseOK}, } for _, env := range tests { if !IsRealtimeTelemetryFrame(env) { t.Fatalf("IsRealtimeTelemetryFrame(%s/%s) = false, want true", env.Protocol, env.MessageID) } } } func TestIsRealtimeTelemetryFrameUsesCanonicalMQTTParsedFields(t *testing.T) { realtime := FrameEnvelope{ Protocol: ProtocolYutongMQTT, MessageID: "MQTT", ParseStatus: ParseOK, ParsedFields: map[string]any{ "yutong_mqtt.data.latitude": 30.590151, }, } if !IsRealtimeTelemetryFrame(realtime) { t.Fatal("canonical MQTT data field should be classified as realtime") } metadataOnly := realtime metadataOnly.ParsedFields = map[string]any{"yutong_mqtt.metadata.topic": "/ytforward/shln/3"} if IsRealtimeTelemetryFrame(metadataOnly) { t.Fatal("MQTT metadata-only field must not be classified as realtime") } } func TestRequiresVehicleIdentity(t *testing.T) { tests := []struct { name string env FrameEnvelope want bool }{ { name: "gb32960 realtime upload", env: FrameEnvelope{Protocol: ProtocolGB32960, MessageID: "0x02", Parsed: map[string]any{"data_units": []any{map[string]any{"name": "vehicle"}}}, ParseStatus: ParseOK}, want: true, }, { name: "gb32960 platform login", env: FrameEnvelope{Protocol: ProtocolGB32960, MessageID: "0x05", ParseStatus: ParseOK}, want: false, }, { name: "gb32960 command response", env: FrameEnvelope{Protocol: ProtocolGB32960, MessageID: "0x07", ParseStatus: ParseOK}, want: false, }, { name: "jt808 registration", env: FrameEnvelope{Protocol: ProtocolJT808, MessageID: "0x0100", ParseStatus: ParseOK}, want: true, }, { name: "jt808 location", env: FrameEnvelope{Protocol: ProtocolJT808, MessageID: "0x0200", Parsed: map[string]any{"location": map[string]any{"speed_kmh": 1}}, ParseStatus: ParseOK}, want: true, }, { name: "yutong empty data", env: FrameEnvelope{Protocol: ProtocolYutongMQTT, MessageID: "MQTT", Parsed: map[string]any{"data": map[string]any{}}, ParseStatus: ParseOK}, want: false, }, { name: "yutong telemetry data", env: FrameEnvelope{Protocol: ProtocolYutongMQTT, MessageID: "MQTT", Parsed: map[string]any{"data": map[string]any{"TOTAL_MILEAGE": 123}}, ParseStatus: ParseOK}, want: true, }, { name: "bad frame", env: FrameEnvelope{Protocol: ProtocolJT808, MessageID: "0x0200", ParseStatus: ParseBadFrame}, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := RequiresVehicleIdentity(tt.env); got != tt.want { t.Fatalf("RequiresVehicleIdentity() = %v, want %v", got, tt.want) } }) } } func TestNormalizeSourceEndpointKey(t *testing.T) { tests := map[string]string{ "115.231.168.135:20215": "115.231.168.135", "115.231.168.135": "115.231.168.135", " 115.159.85.149:28316 ": "115.159.85.149", "mqtt://yutong/ytforward/shln/3": "mqtt", "MQTT://YUTONG/topic": "mqtt", "": "", " ": "", } for input, want := range tests { if got := NormalizeSourceEndpointKey(input); got != want { t.Fatalf("NormalizeSourceEndpointKey(%q) = %q, want %q", input, got, want) } } }