package gateway import ( "context" "encoding/hex" "io" "log/slog" "net" "strings" "testing" "time" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/metrics" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/protocol/gb32960" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/protocol/jt808" ) func TestTCPServerPublishesGoodFrameOnlyToRawByDefault(t *testing.T) { frame, err := hex.DecodeString("7E020000320133077954250001000000000048000301D2C4C707376139000A00E6004F26063016235701040001900C2504000000000202000030011F31010F867E") if err != nil { t.Fatal(err) } sink := &recordingSink{} server := newTestServer(t, TCPProtocol{ Protocol: envelope.ProtocolJT808, Addr: ":0", Extract: jt808.ExtractFrames, Parse: jt808.ParseFrame, }, sink) client, done := runPipe(t, server) if _, err := client.Write(frame); err != nil { t.Fatalf("client.Write() error = %v", err) } _ = client.Close() <-done if len(sink.raw) != 1 || len(sink.unified) != 0 { t.Fatalf("raw=%d unified=%d", len(sink.raw), len(sink.unified)) } if sink.raw[0].Phone != "13307795425" { t.Fatalf("phone = %q", sink.raw[0].Phone) } if sink.raw[0].Fields[envelope.FieldTotalMileageKM] != 10241.2 { t.Fatalf("total mileage = %#v", sink.raw[0].Fields[envelope.FieldTotalMileageKM]) } } func TestTCPServerRecordsFrameMetrics(t *testing.T) { registry := metrics.NewRegistry() server, err := NewTCPServer(TCPServerConfig{ Protocol: TCPProtocol{ Protocol: envelope.ProtocolJT808, Addr: ":0", Extract: jt808.ExtractFrames, Parse: func(_ []byte, receivedAtMS int64, sourceEndpoint string) (envelope.FrameEnvelope, error) { return envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, MessageID: "0x0200", Phone: "13307795425", SourceEndpoint: sourceEndpoint, ReceivedAtMS: receivedAtMS, EventTimeMS: receivedAtMS, ParseStatus: envelope.ParseOK, }, nil }, }, Sink: &recordingSink{}, Logger: slog.New(slog.NewTextHandler(testWriter{t: t}, nil)), Metrics: registry, }) if err != nil { t.Fatalf("NewTCPServer() error = %v", err) } server.handleFrame(context.Background(), nil, []byte{0x01}, "127.0.0.1:808", &connectionState{}) text := registry.Render() for _, want := range []string{ `vehicle_gateway_frames_total{protocol="JT808",status="OK"} 1`, `vehicle_gateway_identity_total{protocol="JT808",status="unresolved"} 1`, `vehicle_gateway_publish_total{kind="raw",protocol="JT808",status="ok"} 1`, `vehicle_gateway_frame_duration_ms{protocol="JT808",status="OK"}`, `vehicle_gateway_frame_duration_ms_histogram_bucket{le="+Inf",protocol="JT808",status="OK"} 1`, `vehicle_gateway_frame_duration_ms_histogram_count{protocol="JT808",status="OK"} 1`, `vehicle_gateway_frame_duration_ms_histogram_sum{protocol="JT808",status="OK"}`, } { if !strings.Contains(text, want) { t.Fatalf("metrics missing %s:\n%s", want, text) } } if strings.Contains(text, `kind="unified"`) { t.Fatalf("unified publish metric should not be recorded by default:\n%s", text) } } func TestTCPServerRecordsActiveConnectionGauge(t *testing.T) { registry := metrics.NewRegistry() server, err := NewTCPServer(TCPServerConfig{ Protocol: TCPProtocol{ Protocol: envelope.ProtocolJT808, Addr: ":0", Extract: jt808.ExtractFrames, Parse: jt808.ParseFrame, }, Sink: &recordingSink{}, Logger: slog.New(slog.NewTextHandler(testWriter{t: t}, nil)), Metrics: registry, IdleTimeout: time.Minute, }) if err != nil { t.Fatalf("NewTCPServer() error = %v", err) } client, srv := net.Pipe() opened := make(chan struct{}) done := make(chan struct{}) go func() { close(opened) server.handleConnection(context.Background(), srv) close(done) }() <-opened deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { if strings.Contains(registry.Render(), `vehicle_gateway_active_connections{protocol="JT808"} 1`) { break } time.Sleep(10 * time.Millisecond) } if text := registry.Render(); !strings.Contains(text, `vehicle_gateway_active_connections{protocol="JT808"} 1`) { t.Fatalf("active connection gauge missing while open:\n%s", text) } _ = client.Close() <-done if text := registry.Render(); !strings.Contains(text, `vehicle_gateway_active_connections{protocol="JT808"} 0`) { t.Fatalf("active connection gauge should return to zero:\n%s", text) } } func TestTCPServerRecordsConnectionRejectionMetric(t *testing.T) { registry := metrics.NewRegistry() server, err := NewTCPServer(TCPServerConfig{ Protocol: TCPProtocol{ Protocol: envelope.ProtocolJT808, Addr: ":0", Extract: jt808.ExtractFrames, Parse: jt808.ParseFrame, }, Sink: &recordingSink{}, Logger: slog.New(slog.NewTextHandler(testWriter{t: t}, nil)), Metrics: registry, }) if err != nil { t.Fatalf("NewTCPServer() error = %v", err) } server.recordConnectionRejection("max_connections") if text := registry.Render(); !strings.Contains(text, `vehicle_gateway_connection_rejections_total{protocol="JT808",reason="max_connections"} 1`) { t.Fatalf("connection rejection metric missing:\n%s", text) } } func TestTCPServerRecordsConnectionCloseReasonMetric(t *testing.T) { registry := metrics.NewRegistry() server, err := NewTCPServer(TCPServerConfig{ Protocol: TCPProtocol{ Protocol: envelope.ProtocolJT808, Addr: ":0", Extract: jt808.ExtractFrames, Parse: jt808.ParseFrame, }, Sink: &recordingSink{}, Logger: slog.New(slog.NewTextHandler(testWriter{t: t}, nil)), Metrics: registry, }) if err != nil { t.Fatalf("NewTCPServer() error = %v", err) } server.recordConnectionClose("read_timeout") if text := registry.Render(); !strings.Contains(text, `vehicle_gateway_connection_closes_total{protocol="JT808",reason="read_timeout"} 1`) { t.Fatalf("connection close metric missing:\n%s", text) } } func TestTCPServerPublishesBadFrameOnlyToRaw(t *testing.T) { good := buildGBFrame(0x02, 0xfe, "LNBSCB3D4R1234567", nil) good[len(good)-1] ^= 0xff sink := &recordingSink{} registry := metrics.NewRegistry() server := newTestServer(t, TCPProtocol{ Protocol: envelope.ProtocolGB32960, Addr: ":0", Extract: gb32960.ExtractFrames, Parse: gb32960.ParseFrame, }, sink) server.metrics = registry client, done := runPipe(t, server) if _, err := client.Write(good); err != nil { t.Fatalf("client.Write() error = %v", err) } _ = client.Close() <-done if len(sink.raw) != 1 || len(sink.unified) != 0 { t.Fatalf("raw=%d unified=%d", len(sink.raw), len(sink.unified)) } if sink.raw[0].ParseStatus != envelope.ParseBadFrame { t.Fatalf("parse status = %q", sink.raw[0].ParseStatus) } if sink.raw[0].ParseError == "" { t.Fatal("parse error should be recorded") } if text := registry.Render(); !strings.Contains(text, `vehicle_gateway_parse_errors_total{protocol="GB32960",reason="checksum"} 1`) { t.Fatalf("parse error metric missing:\n%s", text) } } func TestTCPServerAnnotatesUnresolvedIdentity(t *testing.T) { sink := &recordingSink{} server := newTestServer(t, TCPProtocol{ Protocol: envelope.ProtocolJT808, Addr: ":0", Extract: func(raw []byte) ([][]byte, []byte, error) { return [][]byte{raw}, nil, nil }, Parse: func(_ []byte, receivedAtMS int64, sourceEndpoint string) (envelope.FrameEnvelope, error) { return envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, MessageID: "0x0200", Phone: "13307795425", SourceEndpoint: sourceEndpoint, ReceivedAtMS: receivedAtMS, EventTimeMS: receivedAtMS, ParseStatus: envelope.ParseOK, }, nil }, }, sink) server.handleFrame(context.Background(), nil, []byte{0x01}, "127.0.0.1:808", &connectionState{}) if len(sink.raw) != 1 { t.Fatalf("raw count = %d", len(sink.raw)) } identity, ok := sink.raw[0].Parsed["identity"].(map[string]any) if !ok || identity["resolved"] != false || identity["reason"] != "no_binding" { t.Fatalf("identity metadata = %#v", sink.raw[0].Parsed["identity"]) } } func TestTCPServerWritesProtocolResponseAfterPublish(t *testing.T) { frame := buildGBFrame(0x07, 0xfe, "LNBSCB3D4R1234567", nil) sink := &recordingSink{} server := newTestServer(t, TCPProtocol{ Protocol: envelope.ProtocolGB32960, Addr: ":0", Extract: gb32960.ExtractFrames, Parse: gb32960.ParseFrame, Respond: func(_ []byte, env envelope.FrameEnvelope) ([]byte, bool, error) { if len(sink.raw) != 1 || sink.raw[0].EventID != env.EventID { t.Fatalf("response built before raw publish: raw=%d unified=%d", len(sink.raw), len(sink.unified)) } return []byte("ACK"), true, nil }, }, sink) client, done := runPipe(t, server) if _, err := client.Write(frame); err != nil { t.Fatalf("client.Write() error = %v", err) } buf := make([]byte, 3) if _, err := io.ReadFull(client, buf); err != nil { t.Fatalf("read response error = %v", err) } if string(buf) != "ACK" { t.Fatalf("response = %q", string(buf)) } _ = client.Close() <-done } func TestTCPServerUsesUncancelledFrameContextForAlreadyReadFrame(t *testing.T) { parent, cancel := context.WithCancel(context.Background()) cancel() resolver := &contextCheckingResolver{} sink := &contextCheckingSink{} server, err := NewTCPServer(TCPServerConfig{ Protocol: TCPProtocol{ Protocol: envelope.ProtocolJT808, Addr: ":0", Extract: jt808.ExtractFrames, Parse: func(_ []byte, receivedAtMS int64, sourceEndpoint string) (envelope.FrameEnvelope, error) { return envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, MessageID: "0x0200", Phone: "13307795425", SourceEndpoint: sourceEndpoint, ReceivedAtMS: receivedAtMS, EventTimeMS: receivedAtMS, ParseStatus: envelope.ParseOK, }, nil }, }, Sink: sink, Resolver: resolver, Logger: slog.New(slog.NewTextHandler(testWriter{t: t}, nil)), }) if err != nil { t.Fatalf("NewTCPServer() error = %v", err) } server.handleFrame(parent, nil, []byte{0x01}, "127.0.0.1:808", &connectionState{}) if resolver.ctxErr != nil { t.Fatalf("resolver saw cancelled context: %v", resolver.ctxErr) } if sink.rawCtxErr != nil { t.Fatalf("raw publish saw cancelled context: %v", sink.rawCtxErr) } if sink.unifiedCtxErr != nil { t.Fatalf("unified publish saw cancelled context: %v", sink.unifiedCtxErr) } if sink.rawCount != 1 || sink.unifiedCount != 0 { t.Fatalf("raw=%d unified=%d", sink.rawCount, sink.unifiedCount) } } func TestTCPServerPublishesUnifiedWhenExplicitlyEnabled(t *testing.T) { sink := &recordingSink{} server := newTestServer(t, TCPProtocol{ Protocol: envelope.ProtocolJT808, Addr: ":0", Extract: jt808.ExtractFrames, Parse: func(_ []byte, receivedAtMS int64, sourceEndpoint string) (envelope.FrameEnvelope, error) { return envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, MessageID: "0x0200", Phone: "13307795425", SourceEndpoint: sourceEndpoint, ReceivedAtMS: receivedAtMS, EventTimeMS: receivedAtMS, ParseStatus: envelope.ParseOK, }, nil }, }, sink) server.publishUnified = true server.handleFrame(context.Background(), nil, []byte{0x01}, "127.0.0.1:808", &connectionState{}) if len(sink.raw) != 1 || len(sink.unified) != 1 { t.Fatalf("raw=%d unified=%d", len(sink.raw), len(sink.unified)) } } type contextCheckingResolver struct { ctxErr error } func (r *contextCheckingResolver) Resolve(ctx context.Context, env envelope.FrameEnvelope) (envelope.FrameEnvelope, error) { r.ctxErr = ctx.Err() return env, r.ctxErr } type contextCheckingSink struct { rawCtxErr error unifiedCtxErr error fieldsCtxErr error rawCount int unifiedCount int fieldsCount int } func (s *contextCheckingSink) PublishRaw(ctx context.Context, _ envelope.FrameEnvelope) error { s.rawCtxErr = ctx.Err() s.rawCount++ return s.rawCtxErr } func (s *contextCheckingSink) PublishUnified(ctx context.Context, _ envelope.FrameEnvelope) error { s.unifiedCtxErr = ctx.Err() s.unifiedCount++ return s.unifiedCtxErr } func (s *contextCheckingSink) PublishFields(ctx context.Context, _ envelope.FrameEnvelope) error { s.fieldsCtxErr = ctx.Err() s.fieldsCount++ return s.fieldsCtxErr } func (s *contextCheckingSink) Close() error { return nil } func newTestServer(t *testing.T, protocol TCPProtocol, sink *recordingSink) *TCPServer { t.Helper() server, err := NewTCPServer(TCPServerConfig{ Protocol: protocol, Sink: sink, Logger: slog.New(slog.NewTextHandler(testWriter{t: t}, nil)), ReadBufferSize: 1024, IdleTimeout: time.Second, MaxConnections: 1, }) if err != nil { t.Fatalf("NewTCPServer() error = %v", err) } return server } func runPipe(t *testing.T, server *TCPServer) (net.Conn, <-chan struct{}) { t.Helper() client, srv := net.Pipe() done := make(chan struct{}) go func() { defer close(done) server.handleConnection(context.Background(), srv) }() return client, done } type recordingSink struct { raw []envelope.FrameEnvelope unified []envelope.FrameEnvelope fields []envelope.FrameEnvelope } func (s *recordingSink) PublishRaw(_ context.Context, env envelope.FrameEnvelope) error { s.raw = append(s.raw, env) return nil } func (s *recordingSink) PublishUnified(_ context.Context, env envelope.FrameEnvelope) error { s.unified = append(s.unified, env) return nil } func (s *recordingSink) PublishFields(_ context.Context, env envelope.FrameEnvelope) error { s.fields = append(s.fields, env) return nil } func (s *recordingSink) Close() error { return nil } type testWriter struct { t *testing.T } func (w testWriter) Write(p []byte) (int, error) { w.t.Log(string(p)) return len(p), nil } func buildGBFrame(command byte, response byte, vin string, body []byte) []byte { frame := []byte{'#', '#', command, response} vinBytes := []byte(vin) if len(vinBytes) < 17 { vinBytes = append(vinBytes, make([]byte, 17-len(vinBytes))...) } frame = append(frame, vinBytes[:17]...) frame = append(frame, 0x01, byte(len(body)>>8), byte(len(body))) frame = append(frame, body...) var bcc byte for _, value := range frame[2:] { bcc ^= value } return append(frame, bcc) }