package eventbus import ( "context" "encoding/json" "errors" "strings" "testing" "time" "github.com/nats-io/nats.go" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) func TestNATSSinkRoutesRawAndUnifiedSubjects(t *testing.T) { publisher := &recordingNATSPublisher{} sink := newNATSSinkWithPublisher(publisher, NATSConfig{ RawSubjects: map[envelope.Protocol]string{ envelope.ProtocolJT808: "vehicle.raw.jt808.v1", }, UnifiedSubject: "vehicle.event.unified.v1", }) 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 err := sink.PublishUnified(context.Background(), env); err != nil { t.Fatalf("PublishUnified() error = %v", err) } if got, want := publisher.messages[0].subject, "vehicle.raw.jt808.v1"; got != want { t.Fatalf("raw subject = %q, want %q", got, want) } if got, want := publisher.messages[1].subject, "vehicle.event.unified.v1"; got != want { t.Fatalf("unified subject = %q, want %q", got, want) } var decoded envelope.FrameEnvelope if err := json.Unmarshal(publisher.messages[0].data, &decoded); err != nil { t.Fatalf("raw payload is not envelope json: %v", err) } if decoded.Phone != "13307795425" { t.Fatalf("decoded phone = %q", decoded.Phone) } } func TestNATSSinkDefaultsToGoRawSubjects(t *testing.T) { publisher := &recordingNATSPublisher{} sink := newNATSSinkWithPublisher(publisher, NATSConfig{}) err := sink.PublishRaw(context.Background(), envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, Phone: "13307795425", ReceivedAtMS: 1782918600000, }) if err != nil { t.Fatalf("PublishRaw() error = %v", err) } if got, want := publisher.messages[0].subject, "vehicle.raw.go.jt808.v1"; got != want { t.Fatalf("subject = %q, want %q", got, want) } } func TestNATSSinkPublishesDurableRecords(t *testing.T) { publisher := &recordingNATSPublisher{} sink := newNATSSinkWithPublisher(publisher, NATSConfig{ RawSubjects: map[envelope.Protocol]string{ envelope.ProtocolJT808: "vehicle.raw.go.jt808.v1", }, FieldsSubjects: map[envelope.Protocol]string{ envelope.ProtocolJT808: "vehicle.fields.go.jt808.v1", }, UnifiedSubject: "vehicle.event.go.unified.v1", }) env := envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808, Phone: "13307795425", MessageID: "0x0200"} err := sink.PublishRecords(context.Background(), []durableRecord{ {Kind: "raw", Envelope: env}, {Kind: "fields", Envelope: env}, {Kind: "unified", Envelope: env}, }) if err != nil { t.Fatalf("PublishRecords() error = %v", err) } if len(publisher.messages) != 3 { t.Fatalf("published messages = %d, want 3", len(publisher.messages)) } for i, want := range []string{ "vehicle.raw.go.jt808.v1", "vehicle.fields.go.jt808.v1", "vehicle.event.go.unified.v1", } { if got := publisher.messages[i].subject; got != want { t.Fatalf("message %d subject = %q, want %q", i, got, want) } } } func TestNATSMessageIDSeparatesKindAndSubject(t *testing.T) { env := envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808, Phone: "13307795425", MessageID: "0x0200", Sequence: 7} raw := natsMessageID("raw", "vehicle.raw.go.jt808.v1", env) rawRetry := natsMessageID("raw", "vehicle.raw.go.jt808.v1", env) fields := natsMessageID("fields", "vehicle.fields.go.jt808.v1", env) unified := natsMessageID("unified", "vehicle.event.go.unified.v1", env) rawOtherSubject := natsMessageID("raw", "vehicle.raw.go.gb32960.v1", env) if raw != rawRetry { t.Fatalf("same kind/subject/event id should be stable: %q vs %q", raw, rawRetry) } if raw == fields || raw == unified || raw == rawOtherSubject { t.Fatalf("message ids should be unique per kind and subject: raw=%q fields=%q unified=%q rawOther=%q", raw, fields, unified, rawOtherSubject) } if !strings.Contains(raw, env.StableEventID()) { t.Fatalf("message id %q should retain stable event id %q", raw, env.StableEventID()) } } func TestNATSSinkPublishRecordsRejectsUnknownKind(t *testing.T) { sink := newNATSSinkWithPublisher(&recordingNATSPublisher{}, NATSConfig{}) err := sink.PublishRecords(context.Background(), []durableRecord{ {Kind: "unknown", Envelope: envelope.FrameEnvelope{Protocol: envelope.ProtocolJT808}}, }) if err == nil || !strings.Contains(err.Error(), "unknown durable record kind") { t.Fatalf("PublishRecords() error = %v, want unknown kind", err) } } func TestNATSSinkAsyncPublishCompletesOnlyAfterPubAck(t *testing.T) { future := newTestPubAckFuture() asyncPublisher := &recordingNATSAsyncPublisher{future: future} sink := newNATSSinkWithPublishers(&recordingNATSPublisher{}, asyncPublisher, NATSConfig{}) record := durableRecord{Kind: "raw", Envelope: normalizeDurableEnvelope(durableTestEnvelope())} completed := make(chan error, 1) if err := sink.PublishRecordAsync(record, func(err error) { completed <- err }); err != nil { t.Fatalf("PublishRecordAsync() error = %v", err) } select { case err := <-completed: t.Fatalf("completion fired before PubAck: %v", err) case <-time.After(10 * time.Millisecond): } if got, want := asyncPublisher.subject, "vehicle.raw.go.jt808.v1"; got != want { t.Fatalf("async subject = %q, want %q", got, want) } var decoded envelope.FrameEnvelope if err := json.Unmarshal(asyncPublisher.data, &decoded); err != nil { t.Fatalf("decode async payload: %v", err) } if decoded.EventID != record.Envelope.EventID { t.Fatalf("async event id = %q, want %q", decoded.EventID, record.Envelope.EventID) } future.ok <- &nats.PubAck{Stream: "VEHICLE_RAW", Sequence: 1} select { case err := <-completed: if err != nil { t.Fatalf("completion error = %v", err) } case <-time.After(time.Second): t.Fatal("completion did not fire after PubAck") } } func TestNATSSinkAsyncPublishPropagatesFutureError(t *testing.T) { future := newTestPubAckFuture() sink := newNATSSinkWithPublishers( &recordingNATSPublisher{}, &recordingNATSAsyncPublisher{future: future}, NATSConfig{}, ) completed := make(chan error, 1) errWant := errors.New("jetstream ack timeout") if err := sink.PublishRecordAsync(durableRecord{ Kind: "raw", Envelope: normalizeDurableEnvelope(durableTestEnvelope()), }, func(err error) { completed <- err }); err != nil { t.Fatalf("PublishRecordAsync() error = %v", err) } future.err <- errWant select { case err := <-completed: if !errors.Is(err, errWant) { t.Fatalf("completion error = %v, want %v", err, errWant) } case <-time.After(time.Second): t.Fatal("completion did not fire after future error") } } func TestNATSSinkValidatesDurableRecordBeforeOutboxPersistence(t *testing.T) { sink := newNATSSinkWithPublisher(&recordingNATSPublisher{}, NATSConfig{}) err := sink.ValidateRecord(durableRecord{ Kind: "raw", Envelope: envelope.FrameEnvelope{Protocol: envelope.Protocol("UNKNOWN")}, }) if err == nil || !strings.Contains(err.Error(), "raw subject not configured") { t.Fatalf("ValidateRecord() error = %v", err) } } func TestValidateNATSConfigRejectsRawFieldsSubjectOverlap(t *testing.T) { err := ValidateNATSConfig(NATSConfig{ RawSubjects: map[envelope.Protocol]string{ envelope.ProtocolJT808: "vehicle.same.jt808", }, FieldsSubjects: map[envelope.Protocol]string{ envelope.ProtocolJT808: "vehicle.same.jt808", }, }) if err == nil { t.Fatal("ValidateNATSConfig() error = nil, want overlap rejection") } if !strings.Contains(err.Error(), "nats subject") { t.Fatalf("error = %q, want nats subject hint", err) } } func TestValidateNATSConfigRejectsKnownProtocolSubjectMismatch(t *testing.T) { err := ValidateNATSConfig(NATSConfig{ RawSubjects: map[envelope.Protocol]string{ envelope.ProtocolJT808: "vehicle.raw.go.gb32960.v1", }, }) if err == nil { t.Fatal("ValidateNATSConfig() error = nil, want raw protocol mismatch") } if !strings.Contains(err.Error(), "must match protocol") { t.Fatalf("error = %q, want protocol mismatch hint", err) } err = ValidateNATSConfig(NATSConfig{ FieldsSubjects: map[envelope.Protocol]string{ envelope.ProtocolGB32960: "vehicle.fields.go.yutong-mqtt.v1", }, }) if err == nil { t.Fatal("ValidateNATSConfig() error = nil, want fields protocol mismatch") } if !strings.Contains(err.Error(), "must match protocol") { t.Fatalf("error = %q, want protocol mismatch hint", err) } } type recordingNATSPublisher struct { messages []recordedNATSMessage } type recordedNATSMessage struct { subject string data []byte } func (p *recordingNATSPublisher) Publish(_ context.Context, subject string, data []byte, _ ...NATSPublishOption) error { p.messages = append(p.messages, recordedNATSMessage{subject: subject, data: append([]byte(nil), data...)}) return nil } type recordingNATSAsyncPublisher struct { subject string data []byte future nats.PubAckFuture err error } func (p *recordingNATSAsyncPublisher) PublishAsync(subject string, data []byte, _ ...NATSPublishOption) (nats.PubAckFuture, error) { p.subject = subject p.data = append([]byte(nil), data...) return p.future, p.err } type testPubAckFuture struct { ok chan *nats.PubAck err chan error msg *nats.Msg } func newTestPubAckFuture() *testPubAckFuture { return &testPubAckFuture{ ok: make(chan *nats.PubAck, 1), err: make(chan error, 1), msg: &nats.Msg{}, } } func (f *testPubAckFuture) Ok() <-chan *nats.PubAck { return f.ok } func (f *testPubAckFuture) Err() <-chan error { return f.err } func (f *testPubAckFuture) Msg() *nats.Msg { return f.msg }