295 lines
9.4 KiB
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 }
|