package eventbus import ( "context" "encoding/json" "reflect" "strings" "testing" "github.com/segmentio/kafka-go" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) func TestKafkaSinkRoutesRawTopics(t *testing.T) { writer := &recordingWriter{} sink := newKafkaSinkWithWriter(writer, KafkaConfig{}) env := envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808, Phone: "13307795425", MessageID: "0x0200"} if err := sink.PublishRaw(context.Background(), env); err != nil { t.Fatalf("PublishRaw() error = %v", err) } if len(writer.messages) != 1 { t.Fatalf("messages = %d", len(writer.messages)) } msg := writer.messages[0] if got, want := msg.Topic, "vehicle.raw.go.jt808.v1"; got != want { t.Fatalf("topic = %q, want %q", got, want) } if string(msg.Key) != "JT808:13307795425" { t.Fatalf("key = %q", string(msg.Key)) } var decoded envelope.FrameEnvelope if err := json.Unmarshal(msg.Value, &decoded); err != nil { t.Fatalf("json.Unmarshal() error = %v", err) } if decoded.EventID == "" || decoded.ParseStatus != envelope.ParseOK { t.Fatalf("unexpected payload defaults: %#v", decoded) } } func TestKafkaSinkRoutesUnifiedTopic(t *testing.T) { writer := &recordingWriter{} sink := newKafkaSinkWithWriter(writer, KafkaConfig{UnifiedTopic: "custom.unified"}) env := envelope.FrameEnvelope{Protocol: envelope.ProtocolGB32960, VIN: "LNBVIN00000000001", MessageID: "0x02"} if err := sink.PublishUnified(context.Background(), env); err != nil { t.Fatalf("PublishUnified() error = %v", err) } if writer.messages[0].Topic != "custom.unified" { t.Fatalf("topic = %q", writer.messages[0].Topic) } if string(writer.messages[0].Key) != "LNBVIN00000000001" { t.Fatalf("key = %q", string(writer.messages[0].Key)) } } func TestKafkaSinkRejectsUnknownProtocol(t *testing.T) { writer := &recordingWriter{} sink := newKafkaSinkWithWriter(writer, KafkaConfig{}) err := sink.PublishRaw(context.Background(), envelope.FrameEnvelope{Protocol: envelope.Protocol("UNKNOWN")}) if err == nil { t.Fatal("expected unknown protocol error") } } func TestKafkaSinkPublishesDurableRecordsInSingleWriterCall(t *testing.T) { writer := &recordingWriter{} sink := newKafkaSinkWithWriter(writer, KafkaConfig{UnifiedTopic: "custom.unified"}) env := envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808, Phone: "13307795425", MessageID: "0x0200"} err := sink.PublishRecords(context.Background(), []durableRecord{ {Kind: "raw", Envelope: env}, {Kind: "unified", Envelope: env}, }) if err != nil { t.Fatalf("PublishRecords() error = %v", err) } if writer.writeCalls != 1 { t.Fatalf("WriteMessages calls = %d, want 1", writer.writeCalls) } if len(writer.messages) != 2 { t.Fatalf("messages = %d, want 2", len(writer.messages)) } if writer.messages[0].Topic != "vehicle.raw.go.jt808.v1" || writer.messages[1].Topic != "custom.unified" { t.Fatalf("topics = %q, %q", writer.messages[0].Topic, writer.messages[1].Topic) } } func TestNewKafkaSinkUsesProductionDeliveryGuarantees(t *testing.T) { sink, err := NewKafkaSink(KafkaConfig{Brokers: []string{"127.0.0.1:9092"}}) if err != nil { t.Fatalf("NewKafkaSink() error = %v", err) } defer sink.Close() writer, ok := sink.writer.(*kafka.Writer) if !ok { t.Fatalf("writer type = %s", reflect.TypeOf(sink.writer)) } if writer.RequiredAcks != kafka.RequireAll { t.Fatalf("RequiredAcks = %v, want %v", writer.RequiredAcks, kafka.RequireAll) } if writer.Async { t.Fatal("writer must publish synchronously so Kafka errors propagate") } if writer.AllowAutoTopicCreation { t.Fatal("writer must not auto-create production topics") } if _, ok := writer.Balancer.(*kafka.Hash); !ok { t.Fatalf("balancer type = %s, want *kafka.Hash", reflect.TypeOf(writer.Balancer)) } } func TestValidateKafkaConfigRejectsRawFieldsTopicOverlap(t *testing.T) { err := ValidateKafkaConfig(KafkaConfig{ RawTopics: map[envelope.Protocol]string{ envelope.ProtocolJT808: "vehicle.raw.go.jt808.v1", }, FieldsTopics: map[envelope.Protocol]string{ envelope.ProtocolJT808: "vehicle.raw.go.jt808.v1", }, }) if err == nil { t.Fatal("ValidateKafkaConfig() error = nil, want overlap rejection") } if !strings.Contains(err.Error(), "fields kafka topic") { t.Fatalf("error = %q, want fields topic family hint", err) } } func TestValidateKafkaConfigRejectsKnownProtocolTopicMismatch(t *testing.T) { err := ValidateKafkaConfig(KafkaConfig{ RawTopics: map[envelope.Protocol]string{ envelope.ProtocolJT808: "vehicle.raw.go.gb32960.v1", }, }) if err == nil { t.Fatal("ValidateKafkaConfig() error = nil, want raw protocol mismatch") } if !strings.Contains(err.Error(), "must match protocol") { t.Fatalf("error = %q, want protocol mismatch hint", err) } err = ValidateKafkaConfig(KafkaConfig{ FieldsTopics: map[envelope.Protocol]string{ envelope.ProtocolYutongMQTT: "vehicle.fields.go.jt808.v1", }, }) if err == nil { t.Fatal("ValidateKafkaConfig() error = nil, want fields protocol mismatch") } if !strings.Contains(err.Error(), "must match protocol") { t.Fatalf("error = %q, want protocol mismatch hint", err) } } type recordingWriter struct { messages []kafka.Message writeCalls int } func (w *recordingWriter) WriteMessages(_ context.Context, messages ...kafka.Message) error { w.writeCalls++ w.messages = append(w.messages, messages...) return nil } func (w *recordingWriter) Close() error { return nil }