package gateway import ( "context" "encoding/hex" "errors" "fmt" "io" "log/slog" "net" "os" "strings" "syscall" "testing" "time" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/authentication" "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 TestEnrichConnectionPlatformPromotesSourceMetadata(t *testing.T) { env := envelope.FrameEnvelope{ Protocol: envelope.ProtocolGB32960, MessageID: "0x02", ParseStatus: envelope.ParseOK, Fields: map[string]any{}, Parsed: map[string]any{}, } state := &connectionState{platformName: "Hyundai"} enrichConnectionPlatform(&env, state) if env.PlatformName != "Hyundai" || env.SourceCode != "Hyundai" || env.SourceKind != "PLATFORM" { t.Fatalf("source metadata = code:%q platform:%q kind:%q", env.SourceCode, env.PlatformName, env.SourceKind) } if got := fmt.Sprint(env.Fields["platform_account"]); got != "Hyundai" { t.Fatalf("platform_account = %q", got) } if got := fmt.Sprint(env.Parsed["platform_name"]); got != "Hyundai" { t.Fatalf("parsed platform_name = %q", got) } } func TestTCPServerPublishesGoodFrameToRawAndFieldsByDefault(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) server.resolver = vinResolver{vin: "LNBVIN00000000001"} 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 len(sink.fields) != 1 { t.Fatalf("fields=%d, want 1", len(sink.fields)) } if sink.raw[0].Phone != "13307795425" { t.Fatalf("phone = %q", sink.raw[0].Phone) } if sink.raw[0].EventKind != envelope.EventKindRaw { t.Fatalf("raw event kind = %q, want %q", sink.raw[0].EventKind, envelope.EventKindRaw) } if len(sink.raw[0].Fields) != 0 { t.Fatalf("canonical raw must not carry bare standardized fields: %#v", sink.raw[0].Fields) } if got := sink.raw[0].ParsedFields["jt808.location.total_mileage_km"]; got == nil { t.Fatalf("raw parsed fields missing total mileage: %#v", sink.raw[0].ParsedFields) } if got, want := sink.fields[0].Fields["jt808.location.total_mileage_km"], sink.raw[0].ParsedFields["jt808.location.total_mileage_km"]; got != want { t.Fatalf("fields event should reuse raw parsed field, got %#v want %#v", got, want) } if sink.fields[0].EventKind != envelope.EventKindFields { t.Fatalf("fields event kind = %q, want %q", sink.fields[0].EventKind, envelope.EventKindFields) } if got, want := sink.fields[0].SourceEventID, sink.raw[0].StableEventID(); got != want { t.Fatalf("fields source event id = %#v, want %s", got, want) } if sink.fields[0].FieldMapping == "" { t.Fatal("fields event should expose field mapping version") } if len(sink.fields[0].Parsed) != 0 || len(sink.fields[0].ParsedFields) != 0 { t.Fatalf("fields envelope should not duplicate parsed payload: parsed=%#v parsed_fields=%#v", sink.fields[0].Parsed, sink.fields[0].ParsedFields) } } func TestTCPServerRecordsFieldsPublishedMetric(t *testing.T) { frame, err := hex.DecodeString("7E020000320133077954250001000000000048000301D2C4C707376139000A00E6004F26063016235701040001900C2504000000000202000030011F31010F867E") if err != nil { t.Fatal(err) } sink := &recordingSink{} registry := metrics.NewRegistry() server := newTestServer(t, TCPProtocol{ Protocol: envelope.ProtocolJT808, Addr: ":0", Extract: jt808.ExtractFrames, Parse: jt808.ParseFrame, }, sink) server.metrics = registry server.resolver = vinResolver{vin: "LNBVIN00000000001"} client, done := runPipe(t, server) if _, err := client.Write(frame); err != nil { t.Fatalf("client.Write() error = %v", err) } _ = client.Close() <-done text := registry.Render() for _, want := range []string{ `vehicle_gateway_fields_total{protocol="JT808",status="published"} 1`, `vehicle_gateway_fields_count{protocol="JT808",status="published"} `, `vehicle_gateway_fields_count_histogram_count{protocol="JT808",status="published"} 1`, `vehicle_gateway_publish_total{kind="fields",protocol="JT808",status="ok"} 1`, } { if !strings.Contains(text, want) { t.Fatalf("metrics missing %s:\n%s", want, text) } } } 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_last_frame_unix_seconds{protocol="JT808",status="OK"} `, `vehicle_gateway_identity_total{protocol="JT808",status="unresolved"} 1`, `vehicle_gateway_identity_issues_total{message_id="0x0200",protocol="JT808",reason="no_binding",status="unresolved"} 1`, `vehicle_gateway_identity_duration_ms{protocol="JT808",status="unresolved"}`, `vehicle_gateway_identity_duration_ms_histogram_bucket{le="+Inf",protocol="JT808",status="unresolved"} 1`, `vehicle_gateway_identity_duration_ms_histogram_count{protocol="JT808",status="unresolved"} 1`, `vehicle_gateway_identity_duration_ms_histogram_sum{protocol="JT808",status="unresolved"}`, `vehicle_gateway_publish_total{kind="raw",protocol="JT808",status="ok"} 1`, `vehicle_gateway_last_publish_unix_seconds{kind="raw",protocol="JT808",status="ok"} `, `vehicle_gateway_fields_total{protocol="JT808",status="skipped_non_realtime"} 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 TestTCPServerRecordsNonRealtimeFieldsSkipMetric(t *testing.T) { registry := metrics.NewRegistry() sink := &recordingSink{} server := newTestServer(t, TCPProtocol{ Protocol: envelope.ProtocolGB32960, Addr: ":0", Extract: gb32960.ExtractFrames, Parse: gb32960.ParseFrame, }, sink) server.metrics = registry server.handleFrame(context.Background(), nil, buildGBFrame(0x07, 0xfe, "LNBSCB3D4R1234567", nil), "127.0.0.1:32960", &connectionState{}) if len(sink.raw) != 1 { t.Fatalf("raw=%d, want 1", len(sink.raw)) } if len(sink.fields) != 0 { t.Fatalf("fields=%d, want 0", len(sink.fields)) } if text := registry.Render(); !strings.Contains(text, `vehicle_gateway_fields_total{protocol="GB32960",status="skipped_non_realtime"} 1`) { t.Fatalf("non realtime fields skip metric missing:\n%s", text) } } func TestTCPServerSkipsIdentityForGB32960PlatformControlFrame(t *testing.T) { registry := metrics.NewRegistry() sink := &recordingSink{} resolver := &countingResolver{vin: "LNBVIN00000000001"} server := newTestServer(t, TCPProtocol{ Protocol: envelope.ProtocolGB32960, Addr: ":0", Extract: gb32960.ExtractFrames, Parse: gb32960.ParseFrame, }, sink) server.metrics = registry server.resolver = resolver server.handleFrame(context.Background(), nil, buildGBFrame(0x05, 0xfe, "PLATFORMLOGIN0001", nil), "127.0.0.1:32960", &connectionState{}) if resolver.calls != 0 { t.Fatalf("resolver calls = %d, want 0 for platform control frame", resolver.calls) } if len(sink.raw) != 1 { t.Fatalf("raw=%d, want 1", len(sink.raw)) } text := registry.Render() if strings.Contains(text, "vehicle_gateway_identity_total") { t.Fatalf("identity metric should not be recorded for platform control frame:\n%s", text) } if !strings.Contains(text, `vehicle_gateway_identity_skips_total{protocol="GB32960",reason="non_vehicle_frame"} 1`) { t.Fatalf("identity skip metric missing:\n%s", text) } if !strings.Contains(text, `vehicle_gateway_fields_total{protocol="GB32960",status="skipped_non_realtime"} 1`) { t.Fatalf("non realtime fields skip metric missing:\n%s", text) } } func TestTCPServerAuthenticatesAndRedactsGB32960PlatformLogin(t *testing.T) { registry := metrics.NewRegistry() sink := &recordingSink{} server := newTestServer(t, TCPProtocol{ Protocol: envelope.ProtocolGB32960, Addr: ":0", Extract: gb32960.ExtractFrames, Parse: gb32960.ParseFrame, Authenticate: authentication.NewGB32960PlatformAuthenticator(authentication.ModeObserve, map[string][]string{"platform-a": {"secret-a"}}), }, sink) server.metrics = registry body := []byte{0x1a, 0x07, 0x0d, 0x14, 0x00, 0x00, 0x00, 0x01} body = append(body, fixedASCIIBytes("platform-a", 12)...) body = append(body, fixedASCIIBytes("secret-a", 20)...) body = append(body, 0x01) server.handleFrame(context.Background(), nil, buildGBFrame(0x05, 0xfe, "12345678901234567", body), "127.0.0.1:32960", &connectionState{}) if len(sink.raw) != 1 { t.Fatalf("raw=%d, want 1", len(sink.raw)) } raw := sink.raw[0] if raw.AuthenticationMode != "observe" || raw.AuthenticationStatus != authentication.StatusAccepted || raw.AuthenticationEnforced { t.Fatalf("authentication metadata = mode:%q status:%q enforced:%v", raw.AuthenticationMode, raw.AuthenticationStatus, raw.AuthenticationEnforced) } if _, exists := raw.ParsedFields["gb32960.platform_login.password"]; exists { t.Fatalf("plaintext password leaked into parsed fields: %#v", raw.ParsedFields) } if got := raw.ParsedFields["gb32960.platform_login.password_present"]; got != "true" { t.Fatalf("password presence marker = %#v", got) } if text := registry.Render(); !strings.Contains(text, `vehicle_gateway_authentication_total{mode="observe",protocol="GB32960",source="configured",status="accepted"} 1`) { t.Fatalf("authentication metric missing:\n%s", text) } } func TestTCPServerResolvesIdentityForGB32960RealtimeFrame(t *testing.T) { registry := metrics.NewRegistry() sink := &recordingSink{} resolver := &countingResolver{vin: "LNBVIN00000000001"} server := newTestServer(t, TCPProtocol{ Protocol: envelope.ProtocolGB32960, Addr: ":0", Extract: gb32960.ExtractFrames, Parse: gb32960.ParseFrame, }, sink) server.metrics = registry server.resolver = resolver server.handleFrame(context.Background(), nil, buildGBFrame(0x02, 0xfe, "LNBSCB3D4R1234567", nil), "127.0.0.1:32960", &connectionState{}) if resolver.calls != 1 { t.Fatalf("resolver calls = %d, want 1 for realtime frame", resolver.calls) } if len(sink.raw) != 1 { t.Fatalf("raw=%d, want 1", len(sink.raw)) } if sink.raw[0].VIN != "LNBVIN00000000001" { t.Fatalf("raw vin = %q, want resolved vin", sink.raw[0].VIN) } if text := registry.Render(); !strings.Contains(text, `vehicle_gateway_identity_total{protocol="GB32960",status="resolved"} 1`) { t.Fatalf("identity resolved metric missing:\n%s", text) } } func TestTCPServerRecordsIdentityTimeoutMetrics(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{}, Resolver: errorResolver{err: context.DeadlineExceeded}, 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_identity_total{protocol="JT808",status="timeout"} 1`, `vehicle_gateway_identity_issues_total{message_id="0x0200",protocol="JT808",reason="timeout",status="timeout"} 1`, `vehicle_gateway_identity_duration_ms{protocol="JT808",status="timeout"}`, `vehicle_gateway_identity_duration_ms_histogram_bucket{le="+Inf",protocol="JT808",status="timeout"} 1`, `vehicle_gateway_identity_duration_ms_histogram_count{protocol="JT808",status="timeout"} 1`, `vehicle_gateway_frames_total{protocol="JT808",status="PARTIAL"} 1`, } { if !strings.Contains(text, want) { t.Fatalf("metrics missing %s:\n%s", want, text) } } } func TestTCPServerPreservesResolvedEnvelopeWhenIdentitySideEffectFails(t *testing.T) { registry := metrics.NewRegistry() sink := &recordingSink{} wantErr := errors.New("registration upsert failed") 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, ParsedFields: map[string]any{ "jt808.location.total_mileage_km": "10241.2", }, ParseStatus: envelope.ParseOK, }, nil }, }, Sink: sink, Resolver: resolvedErrorResolver{vin: "LNBVIN00000000001", err: wantErr}, 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{}) if len(sink.raw) != 1 { t.Fatalf("raw count = %d, want 1", len(sink.raw)) } if sink.raw[0].VIN != "LNBVIN00000000001" { t.Fatalf("raw vin = %q, want resolved vin", sink.raw[0].VIN) } if sink.raw[0].ParseStatus != envelope.ParsePartial { t.Fatalf("parse status = %q, want PARTIAL", sink.raw[0].ParseStatus) } if len(sink.raw[0].Parsed) != 0 { t.Fatalf("canonical raw should not duplicate parsed tree: %#v", sink.raw[0].Parsed) } if got := sink.raw[0].ParsedFields["jt808.location.total_mileage_km"]; got != "10241.2" { t.Fatalf("protocol field lost after identity side-effect failure: %#v", got) } for field := range sink.raw[0].ParsedFields { if strings.HasPrefix(field, "jt808.identity.") { t.Fatalf("derived identity annotation leaked into protocol fields: %s", field) } } if len(sink.fields) != 1 || sink.fields[0].VIN != "LNBVIN00000000001" { t.Fatalf("fields should preserve resolved vin, fields=%#v", sink.fields) } text := registry.Render() for _, want := range []string{ `vehicle_gateway_identity_total{protocol="JT808",status="error"} 1`, `vehicle_gateway_identity_issues_total{message_id="0x0200",protocol="JT808",reason="resolver_error",status="error"} 1`, `vehicle_gateway_frames_total{protocol="JT808",status="PARTIAL"} 1`, `vehicle_gateway_fields_total{protocol="JT808",status="published"} 1`, } { if !strings.Contains(text, want) { t.Fatalf("metric missing %s:\n%s", want, 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 TestTCPServerClosesActiveConnectionWhenContextCancelled(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) } ctx, cancel := context.WithCancel(context.Background()) client, srv := net.Pipe() done := make(chan struct{}) go func() { server.handleConnection(ctx, srv) close(done) }() waitForMetric(t, registry, `vehicle_gateway_active_connections{protocol="JT808"} 1`) cancel() server.closeActiveConnections() defer client.Close() select { case <-done: case <-time.After(time.Second): t.Fatal("connection handler did not exit after context cancellation") } if text := registry.Render(); !strings.Contains(text, `vehicle_gateway_connection_closes_total{protocol="JT808",reason="context_cancelled"} 1`) { t.Fatalf("context cancellation close metric missing:\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 TestTCPServerConnectionLifecycleLogsStayDebug(t *testing.T) { source, err := os.ReadFile("tcp_server.go") if err != nil { t.Fatal(err) } text := string(source) for _, forbidden := range []string{ `Info("tcp connection opened"`, `Info("tcp connection closed"`, `Warn("tcp connection idle timeout"`, `Warn("tcp connection closed by peer"`, } { if strings.Contains(text, forbidden) { t.Fatalf("high-volume connection lifecycle log should not be info/warn: %s", forbidden) } } for _, want := range []string{ `Debug("tcp connection opened"`, `Debug("tcp connection closed"`, `Debug("tcp connection idle timeout"`, `Debug("tcp connection closed by peer"`, } { if !strings.Contains(text, want) { t.Fatalf("connection lifecycle log should remain debug: %s", want) } } } func TestIsRoutineTCPReadCloseClassifiesRemoteCloseNoise(t *testing.T) { tests := []struct { name string err error want bool }{ {name: "eof", err: io.EOF, want: true}, {name: "net closed", err: net.ErrClosed, want: true}, {name: "reset wrapped", err: fmt.Errorf("read tcp: %w", syscall.ECONNRESET), want: true}, {name: "pipe wrapped", err: fmt.Errorf("write tcp: %w", syscall.EPIPE), want: true}, {name: "reset text", err: errors.New("read tcp 172.17.111.55:808->117.132.196.176:22187: read: connection reset by peer"), want: true}, {name: "unexpected", err: errors.New("checksum parser exploded"), want: false}, {name: "nil", err: nil, want: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := isRoutineTCPReadClose(tt.err); got != tt.want { t.Fatalf("isRoutineTCPReadClose(%v) = %v, want %v", tt.err, got, tt.want) } }) } } 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)) } if len(sink.raw[0].Parsed) != 0 || len(sink.raw[0].ParsedFields) != 0 { t.Fatalf("unresolved derived metadata must not enter canonical protocol payload: parsed=%#v fields=%#v", sink.raw[0].Parsed, sink.raw[0].ParsedFields) } } func TestTCPServerWritesProtocolResponseAfterPublish(t *testing.T) { frame := buildGBFrame(0x07, 0xfe, "LNBSCB3D4R1234567", nil) registry := metrics.NewRegistry() 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) server.metrics = registry 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 text := registry.Render() for _, want := range []string{ `vehicle_gateway_response_total{message_id="0x07",protocol="GB32960",status="ok"} 1`, `vehicle_gateway_last_response_unix_seconds{message_id="0x07",protocol="GB32960",status="ok"} `, `vehicle_gateway_response_duration_ms_histogram_count{message_id="0x07",protocol="GB32960",status="ok"} 1`, `vehicle_gateway_response_e2e_recent_p99_ms{protocol="GB32960"} `, `vehicle_gateway_response_e2e_recent_samples{protocol="GB32960"} `, } { if !strings.Contains(text, want) { t.Fatalf("response metric missing %s:\n%s", want, text) } } } func TestTCPServerWritesProtocolResponseWhenFieldsPublishFails(t *testing.T) { registry := metrics.NewRegistry() sink := &recordingSink{fieldsErr: errors.New("fields queue full")} 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", VIN: "LNBVIN00000000001", Phone: "13307795425", SourceEndpoint: sourceEndpoint, ReceivedAtMS: receivedAtMS, EventTimeMS: receivedAtMS, ParsedFields: map[string]any{ "jt808.location.total_mileage_km": "10241.2", }, ParseStatus: envelope.ParseOK, }, nil }, 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", len(sink.raw)) } return []byte("ACK"), true, nil }, }, sink) server.metrics = registry client, done := runPipe(t, server) if _, err := client.Write([]byte{0x01}); 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 if len(sink.fields) != 1 { t.Fatalf("fields publish attempts = %d, want 1", len(sink.fields)) } text := registry.Render() for _, want := range []string{ `vehicle_gateway_response_total{message_id="0x0200",protocol="JT808",status="ok"} 1`, `vehicle_gateway_fields_total{protocol="JT808",status="publish_error"} 1`, `vehicle_gateway_publish_total{kind="fields",protocol="JT808",status="error"} 1`, } { if !strings.Contains(text, want) { t.Fatalf("metric missing %s:\n%s", want, text) } } } 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 vinResolver struct { vin string } func (r vinResolver) Resolve(_ context.Context, env envelope.FrameEnvelope) (envelope.FrameEnvelope, error) { env.VIN = r.vin return env, nil } type countingResolver struct { calls int vin string } func (r *countingResolver) Resolve(_ context.Context, env envelope.FrameEnvelope) (envelope.FrameEnvelope, error) { r.calls++ env.VIN = r.vin return env, nil } type errorResolver struct { err error } func (r errorResolver) Resolve(_ context.Context, env envelope.FrameEnvelope) (envelope.FrameEnvelope, error) { return env, r.err } type resolvedErrorResolver struct { vin string err error } func (r resolvedErrorResolver) Resolve(_ context.Context, env envelope.FrameEnvelope) (envelope.FrameEnvelope, error) { env.VIN = r.vin if env.Parsed == nil { env.Parsed = map[string]any{} } env.Parsed["identity"] = map[string]any{"resolved": true, "source": "test"} env.EventID = env.StableEventID() return env, r.err } 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 } func waitForMetric(t *testing.T, registry *metrics.Registry, want string) { t.Helper() deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { if strings.Contains(registry.Render(), want) { return } time.Sleep(10 * time.Millisecond) } t.Fatalf("metric missing %s:\n%s", want, registry.Render()) } type recordingSink struct { raw []envelope.FrameEnvelope unified []envelope.FrameEnvelope fields []envelope.FrameEnvelope rawErr error unifiedErr error fieldsErr error } func (s *recordingSink) PublishRaw(_ context.Context, env envelope.FrameEnvelope) error { s.raw = append(s.raw, env) return s.rawErr } func (s *recordingSink) PublishUnified(_ context.Context, env envelope.FrameEnvelope) error { s.unified = append(s.unified, env) return s.unifiedErr } func (s *recordingSink) PublishFields(_ context.Context, env envelope.FrameEnvelope) error { s.fields = append(s.fields, env) return s.fieldsErr } 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) } func fixedASCIIBytes(value string, size int) []byte { out := make([]byte, size) copy(out, []byte(value)) return out }