213 lines
7.0 KiB
Go
213 lines
7.0 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|