package topics import ( "strings" "testing" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) func TestValidateKafkaRawFieldsRejectsWrongFamilyPrefix(t *testing.T) { err := ValidateKafkaRawFields( map[string]string{"JT808": FieldsJT808}, map[string]string{"JT808": FieldsJT808}, ) if err == nil { t.Fatal("ValidateKafkaRawFields() error = nil, want raw prefix rejection") } if !strings.Contains(err.Error(), "raw kafka topic") { t.Fatalf("error = %q, want raw kafka topic hint", err) } err = ValidateKafkaRawFields( map[string]string{"JT808": RawJT808}, map[string]string{"JT808": RawJT808}, ) if err == nil { t.Fatal("ValidateKafkaRawFields() error = nil, want fields prefix rejection") } if !strings.Contains(err.Error(), "fields kafka topic") { t.Fatalf("error = %q, want fields kafka topic hint", err) } } func TestValidateRawFieldsDisjointRejectsSharedValue(t *testing.T) { err := ValidateRawFieldsDisjoint( map[string]string{"raw-jt808": "vehicle.same.jt808"}, map[string]string{"fields-jt808": "vehicle.same.jt808"}, "nats subject", ) if err == nil { t.Fatal("ValidateRawFieldsDisjoint() error = nil, want duplicate rejection") } if !strings.Contains(err.Error(), "must be different") { t.Fatalf("error = %q, want disjoint hint", err) } } func TestValidateKnownFieldsTopicProtocolRejectsMismatchedProtocol(t *testing.T) { err := ValidateKnownFieldsTopicProtocol(FieldsGB32960, "JT808") if err == nil { t.Fatal("ValidateKnownFieldsTopicProtocol() error = nil, want mismatch rejection") } if !strings.Contains(err.Error(), "expects protocol GB32960") { t.Fatalf("error = %q, want expected protocol hint", err) } } func TestValidateKnownRawTopicProtocolRejectsMismatchedProtocol(t *testing.T) { err := ValidateKnownRawTopicProtocol(RawJT808, "GB32960") if err == nil { t.Fatal("ValidateKnownRawTopicProtocol() error = nil, want mismatch rejection") } if !strings.Contains(err.Error(), "expects protocol JT808") { t.Fatalf("error = %q, want expected protocol hint", err) } } func TestValidateKnownTopicProtocolAllowsMatchingAndCustomTopics(t *testing.T) { if err := ValidateKnownRawTopicProtocol(RawGB32960, "GB32960"); err != nil { t.Fatalf("ValidateKnownRawTopicProtocol() matching topic error = %v", err) } if err := ValidateKnownRawTopicProtocol("vehicle.raw.custom.g7", "JT808"); err != nil { t.Fatalf("ValidateKnownRawTopicProtocol() custom topic error = %v", err) } if err := ValidateKnownFieldsTopicProtocol(FieldsJT808, "JT808"); err != nil { t.Fatalf("ValidateKnownFieldsTopicProtocol() matching topic error = %v", err) } if err := ValidateKnownFieldsTopicProtocol("vehicle.fields.custom.g7", "JT808"); err != nil { t.Fatalf("ValidateKnownFieldsTopicProtocol() custom topic error = %v", err) } } func TestValidateRawEnvelopeRejectsExplicitNonRawEventKind(t *testing.T) { status, err := ValidateRawEnvelope(RawJT808, envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, EventKind: envelope.EventKindFields, }) if err == nil { t.Fatal("ValidateRawEnvelope() error = nil, want event kind mismatch") } if status != "event_kind_mismatch" { t.Fatalf("status = %q, want event_kind_mismatch", status) } if !strings.Contains(err.Error(), "expects event kind RAW") { t.Fatalf("error = %q, want RAW hint", err) } } func TestValidateRawEnvelopeAllowsHistoricalEmptyAndRawEventKind(t *testing.T) { for _, kind := range []envelope.EventKind{"", envelope.EventKindRaw} { if status, err := ValidateRawEnvelope(RawGB32960, envelope.FrameEnvelope{Protocol: envelope.ProtocolGB32960, EventKind: kind}); err != nil { t.Fatalf("ValidateRawEnvelope(%q) status=%q error=%v", kind, status, err) } } } func TestValidateFieldsEnvelopeRejectsExplicitNonFieldsEventKind(t *testing.T) { status, err := ValidateFieldsEnvelope(FieldsYutongMQTT, envelope.FrameEnvelope{ Protocol: envelope.ProtocolYutongMQTT, EventKind: envelope.EventKindRaw, }) if err == nil { t.Fatal("ValidateFieldsEnvelope() error = nil, want event kind mismatch") } if status != "event_kind_mismatch" { t.Fatalf("status = %q, want event_kind_mismatch", status) } if !strings.Contains(err.Error(), "expects event kind FIELDS") { t.Fatalf("error = %q, want FIELDS hint", err) } } func TestValidateFieldsEnvelopeRequiresFieldsContract(t *testing.T) { status, err := ValidateFieldsEnvelope(FieldsJT808, envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, EventKind: envelope.EventKindFields, Fields: map[string]any{"jt808.location.total_mileage_km": 10241.2}, }) if err == nil { t.Fatal("ValidateFieldsEnvelope() error = nil, want missing field mapping") } if status != "missing_field_mapping" { t.Fatalf("status = %q, want missing_field_mapping", status) } status, err = ValidateFieldsEnvelope(FieldsJT808, envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, EventKind: envelope.EventKindFields, FieldMapping: "2026-07-03.v1", }) if err == nil { t.Fatal("ValidateFieldsEnvelope() error = nil, want missing fields") } if status != "missing_fields" { t.Fatalf("status = %q, want missing_fields", status) } status, err = ValidateFieldsEnvelope(FieldsJT808, envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, EventKind: envelope.EventKindFields, FieldMapping: "2026-07-03.v1", Fields: map[string]any{"jt808.location.total_mileage_km": 10241.2}, }) if err != nil || status != "" { t.Fatalf("ValidateFieldsEnvelope() status=%q error=%v, want ok", status, err) } } func TestValidateFieldsEnvelopeRejectsNonProtocolFieldNames(t *testing.T) { for _, tt := range []struct { name string topic string protocol envelope.Protocol fields map[string]any }{ { name: "normalized core field", topic: FieldsJT808, protocol: envelope.ProtocolJT808, fields: map[string]any{"total_mileage_km": 10241.2}, }, { name: "other protocol namespace", topic: FieldsGB32960, protocol: envelope.ProtocolGB32960, fields: map[string]any{"jt808.location.latitude": 30.1}, }, { name: "empty suffix", topic: FieldsYutongMQTT, protocol: envelope.ProtocolYutongMQTT, fields: map[string]any{"yutong_mqtt.": 1}, }, } { t.Run(tt.name, func(t *testing.T) { status, err := ValidateFieldsEnvelope(tt.topic, envelope.FrameEnvelope{ Protocol: tt.protocol, EventKind: envelope.EventKindFields, FieldMapping: "2026-07-03.v1", Fields: tt.fields, }) if err == nil { t.Fatal("ValidateFieldsEnvelope() error = nil, want invalid field rejection") } if status != "invalid_field_name" { t.Fatalf("status = %q, want invalid_field_name", status) } if !strings.Contains(err.Error(), "expects every field name") { t.Fatalf("error = %q, want protocol namespace hint", err) } }) } } func TestValidateFieldsEnvelopeAcceptsProtocolNamespaces(t *testing.T) { for _, tt := range []struct { topic string protocol envelope.Protocol field string }{ {topic: FieldsGB32960, protocol: envelope.ProtocolGB32960, field: "gb32960.vehicle.total_mileage_km"}, {topic: FieldsJT808, protocol: envelope.ProtocolJT808, field: "jt808.location.total_mileage_km"}, {topic: FieldsYutongMQTT, protocol: envelope.ProtocolYutongMQTT, field: "yutong_mqtt.root.data.total_mileage"}, } { status, err := ValidateFieldsEnvelope(tt.topic, envelope.FrameEnvelope{ Protocol: tt.protocol, EventKind: envelope.EventKindFields, FieldMapping: "2026-07-03.v1", Fields: map[string]any{tt.field: 1}, }) if err != nil || status != "" { t.Fatalf("ValidateFieldsEnvelope(%s) status=%q error=%v", tt.protocol, status, err) } } } func TestValidateKafkaRawFieldsRejectsKnownProtocolMismatch(t *testing.T) { err := ValidateKafkaRawFields( map[string]string{"JT808": RawGB32960}, map[string]string{"JT808": FieldsJT808}, ) if err == nil { t.Fatal("ValidateKafkaRawFields() error = nil, want known raw protocol mismatch") } if !strings.Contains(err.Error(), "must match protocol") { t.Fatalf("error = %q, want protocol mismatch hint", err) } err = ValidateKafkaRawFields( map[string]string{"JT808": RawJT808}, map[string]string{"JT808": FieldsGB32960}, ) if err == nil { t.Fatal("ValidateKafkaRawFields() error = nil, want known fields protocol mismatch") } if !strings.Contains(err.Error(), "must match protocol") { t.Fatalf("error = %q, want protocol mismatch hint", err) } }