Files
lingniu-vehicle-ingest/go/vehicle-gateway/internal/eventbus/nats_sink_test.go

295 lines
9.4 KiB
Go

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 }