package gateway import ( "context" "encoding/hex" "fmt" "strings" "testing" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/authentication" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/protocol/gb32960" ) func TestFeichiCustom80UsesConnectionLoginAndSurvivesCanonicalRaw(t *testing.T) { payload, _ := hex.DecodeString("3B1428271014282710024E01560000") body := []byte{0x1a, 0x0a, 0x17, 0x0a, 0, 0, 0x80, 0, 15} body = append(body, payload...) body = append(body, 0x80, 0, 3, 0x05, 0x80, 0x02) body = append(body, 0x05, 0, 0x07, 0x36, 0x50, 0x40, 0x01, 0xd2, 0x4f, 0) frame := buildGBFrame(0x02, 0xfe, "LA9GG68L7PBAF4803", body) for _, tc := range []struct { account string password string wantCustom bool }{ {"Feichi", "test-secret", true}, {"Feichi", "wrong-secret", false}, {"Hyundai", "test-secret", false}, {"YueJin", "test-secret", false}, } { t.Run(tc.account+"/"+tc.password, func(t *testing.T) { sink := &recordingSink{} server := newTestServer(t, TCPProtocol{ Protocol: envelope.ProtocolGB32960, Addr: ":0", Extract: gb32960.ExtractFrames, Parse: gb32960.ParseFrame, ParseForPlatform: gb32960.ParseFrameForPlatform, Authenticate: authentication.NewGB32960PlatformAuthenticator(authentication.ModeEnforce, map[string][]string{ "Feichi": {"test-secret"}, "Hyundai": {"test-secret"}, "YueJin": {"test-secret"}, }), }, sink) state := &connectionState{} login := []byte{0x1a, 0x0a, 0x17, 0x0a, 0, 0, 0, 1} login = append(login, fixedASCIIBytes(tc.account, 12)...) login = append(login, fixedASCIIBytes(tc.password, 20)...) login = append(login, 1) server.handleFrame(context.Background(), nil, buildGBFrame(0x05, 0xfe, "", login), "183.239.48.25:2147", state) server.handleFrame(context.Background(), nil, frame, "183.239.48.25:2147", state) if len(sink.raw) != 2 { t.Fatalf("raw frames=%d", len(sink.raw)) } raw := sink.raw[1] if raw.RawHex != strings.ToUpper(hex.EncodeToString(frame)) || len(raw.Parsed) != 0 { t.Fatal("canonical whole RAW frame was changed or contains duplicate tree") } prefix := "gb32960.feichi_custom_80.blocks." _, custom := raw.ParsedFields[prefix+"block_1.payload_hex"] if custom != tc.wantCustom { t.Fatalf("account=%s wantCustom=%v fields=%#v", tc.account, tc.wantCustom, raw.ParsedFields) } if !tc.wantCustom { if raw.ParsedFields["gb32960.gd_fc_demo_extension.declared_length"] != "15" { t.Fatal("legacy decoder was not retained") } return } for key, want := range map[string]string{ prefix + "block_1.declared_length": "15", prefix + "block_1.actual_length": "15", prefix + "block_1.payload_hex": "3B1428271014282710024E01560000", prefix + "block_1.raw_hex": "80000F3B1428271014282710024E01560000", prefix + "block_2.payload_hex": "058002", prefix + "block_2.raw_hex": "800003058002", "gb32960.position.longitude": "121", "gb32960.position.latitude": "30.56", } { if got := fmt.Sprint(raw.ParsedFields[key]); got != want { t.Fatalf("%s=%s want=%s", key, got, want) } } if raw.PlatformName != "Feichi" || raw.ParseStatus != envelope.ParseOK { t.Fatalf("platform=%q status=%s", raw.PlatformName, raw.ParseStatus) } // A fresh TCP connection does not inherit the previous account. server.handleFrame(context.Background(), nil, frame, "183.239.48.25:9999", &connectionState{}) if _, exists := sink.raw[2].ParsedFields[prefix+"block_1.payload_hex"]; exists { t.Fatal("Feichi account leaked into a fresh connection") } }) } } func TestFeichiCustom80TruncationIsPreservedThroughRawProjection(t *testing.T) { sink := &recordingSink{} server := newTestServer(t, TCPProtocol{ Protocol: envelope.ProtocolGB32960, Addr: ":0", Extract: gb32960.ExtractFrames, Parse: gb32960.ParseFrame, ParseForPlatform: gb32960.ParseFrameForPlatform, }, sink) frame := buildGBFrame(0x02, 0xfe, "LA9GG68L7PBAF4803", []byte{0x1a, 0x0a, 0x17, 0x0a, 0, 0, 0x80, 0, 15, 0x3b, 0x14}) server.handleFrame(context.Background(), nil, frame, "183.239.48.25:2147", &connectionState{platformName: "Feichi"}) raw := sink.raw[0] for key, want := range map[string]string{ "gb32960.feichi_custom_80.blocks.block_1.declared_length": "15", "gb32960.feichi_custom_80.blocks.block_1.actual_length": "2", "gb32960.feichi_custom_80.blocks.block_1.payload_hex": "3B14", "gb32960.feichi_custom_80.blocks.block_1.raw_hex": "80000F3B14", "gb32960.feichi_custom_80.blocks.block_1.parse_error": "truncated_payload", } { if got := fmt.Sprint(raw.ParsedFields[key]); got != want { t.Fatalf("%s=%s want=%s", key, got, want) } } if raw.ParseStatus != envelope.ParsePartial || raw.RawHex != strings.ToUpper(hex.EncodeToString(frame)) { t.Fatalf("status=%s raw=%s", raw.ParseStatus, raw.RawHex) } }