package identity import ( "context" "database/sql" "strings" "testing" "github.com/DATA-DOG/go-sqlmock" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) func TestCandidateKeysPrioritizesPhoneDevicePlate(t *testing.T) { keys := CandidateKeys(envelope.FrameEnvelope{ Phone: "013307795425", DeviceID: "D1", Plate: "豫A12345", VehicleKeyHint: "D1", }) got := []string{} for _, key := range keys { got = append(got, key.Column+"="+key.Value) } want := []string{"phone=13307795425", "device_id=D1", "plate=豫A12345", "vin=D1"} if len(got) != len(want) { t.Fatalf("candidate count = %d got=%#v", len(got), got) } for i := range want { if got[i] != want[i] { t.Fatalf("candidate[%d] = %q, want %q", i, got[i], want[i]) } } } func TestCandidateKeysNormalizesPhone(t *testing.T) { keys := CandidateKeys(envelope.FrameEnvelope{ Phone: "013079963379", }) got := []string{} for _, key := range keys { got = append(got, key.Column+"="+key.Value) } want := []string{"phone=13079963379"} if len(got) != len(want) { t.Fatalf("candidate count = %d got=%#v", len(got), got) } for i := range want { if got[i] != want[i] { t.Fatalf("candidate[%d] = %q, want %q", i, got[i], want[i]) } } } func TestMySQLResolverFillsVINFromPhone(t *testing.T) { db, mock := newMockDB(t) defer db.Close() mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE phone = \\?"). WithArgs("13307795425"). WillReturnRows(sqlmock.NewRows([]string{"vin"}).AddRow("LNBVIN00000000001")) resolver := NewMySQLResolver(db, "vehicle_identity_binding") env, err := resolver.Resolve(context.Background(), envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, Phone: "013307795425", Parsed: map[string]any{}, }) if err != nil { t.Fatalf("Resolve() error = %v", err) } if env.VIN != "LNBVIN00000000001" { t.Fatalf("vin = %q", env.VIN) } identity, ok := env.Parsed["identity"].(map[string]any) if !ok || identity["source"] != "phone" { t.Fatalf("identity metadata = %#v", env.Parsed["identity"]) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestMySQLResolverFallsBackToDeviceID(t *testing.T) { db, mock := newMockDB(t) defer db.Close() mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE phone = \\?"). WithArgs("13307795425"). WillReturnError(sql.ErrNoRows) mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE device_id = \\?"). WithArgs("D1"). WillReturnRows(sqlmock.NewRows([]string{"vin"}).AddRow("LNBVIN00000000002")) resolver := NewMySQLResolver(db, "vehicle_identity_binding") env, err := resolver.Resolve(context.Background(), envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, Phone: "013307795425", DeviceID: "D1", }) if err != nil { t.Fatalf("Resolve() error = %v", err) } if env.VIN != "LNBVIN00000000002" { t.Fatalf("vin = %q", env.VIN) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestMySQLResolverTracksJT808RegistrationAndBackfillsBinding(t *testing.T) { db, mock := newMockDB(t) defer db.Close() mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE phone = \\?"). WithArgs("13079963379"). WillReturnError(sql.ErrNoRows) mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE device_id = \\?"). WithArgs("DEV0001"). WillReturnError(sql.ErrNoRows) mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE plate = \\?"). WithArgs("TEST123"). WillReturnRows(sqlmock.NewRows([]string{"vin"}).AddRow("LKLG7C4E3NA774736")) mock.ExpectExec("INSERT INTO jt808_registration"). WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectExec("UPDATE IGNORE vehicle_identity_binding"). WithArgs("13079963379", "13079963379", "DEV0001", "DEV0001", "TEST123", "TEST123", "LKLG7C4E3NA774736"). WillReturnResult(sqlmock.NewResult(0, 1)) resolver := NewMySQLResolver(db, "vehicle_identity_binding") env, err := resolver.Resolve(context.Background(), envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, MessageID: "0x0100", Phone: "013079963379", DeviceID: "DEV0001", Plate: "TEST123", SourceEndpoint: "115.231.168.135:43625", Parsed: map[string]any{ "registration": map[string]any{ "province": uint16(16), "city": uint16(32), "manufacturer": "YUTNG", "device_type": "ZK6105CHEVNPG4", "device_id": "DEV0001", "plate_color": uint8(2), "plate": "TEST123", }, }, }) if err != nil { t.Fatalf("Resolve() error = %v", err) } if env.VIN != "LKLG7C4E3NA774736" { t.Fatalf("vin = %q", env.VIN) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestMySQLResolverTracksFirstJT808LocationThenThrottles(t *testing.T) { db, mock := newMockDB(t) defer db.Close() mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE phone = \\?"). WithArgs("13307795425"). WillReturnError(sql.ErrNoRows) mock.ExpectExec("INSERT INTO jt808_registration"). WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE phone = \\?"). WithArgs("13307795425"). WillReturnError(sql.ErrNoRows) resolver := NewMySQLResolver(db, "vehicle_identity_binding") for i := 0; i < 2; i++ { _, err := resolver.Resolve(context.Background(), envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, MessageID: "0x0200", Phone: "013307795425", SourceEndpoint: "115.231.168.135:43625", Parsed: map[string]any{}, }) if err != nil { t.Fatalf("Resolve() error = %v", err) } } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestMySQLResolverEnsuresMinimalIdentitySchema(t *testing.T) { db, mock := newMockDB(t) defer db.Close() mock.ExpectExec("CREATE TABLE IF NOT EXISTS vehicle_identity_binding"). WillReturnResult(sqlmock.NewResult(0, 0)) mock.ExpectExec("CREATE TABLE IF NOT EXISTS jt808_registration"). WillReturnResult(sqlmock.NewResult(0, 0)) resolver := NewMySQLResolver(db, "vehicle_identity_binding") if err := resolver.EnsureSchema(context.Background()); err != nil { t.Fatalf("EnsureSchema() error = %v", err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestIdentitySchemaUsesBusinessKeysOnly(t *testing.T) { binding := identityBindingTableSQL("vehicle_identity_binding") registration := jt808RegistrationTableSQL for _, sqlText := range []string{binding, registration} { for _, column := range []string{"id BIGINT", "AUTO_INCREMENT", "created_at"} { if strings.Contains(sqlText, column) { t.Fatalf("identity schema should not contain %s:\n%s", column, sqlText) } } } if !strings.Contains(binding, "vin VARCHAR(32) PRIMARY KEY") { t.Fatalf("binding table should key by vin:\n%s", binding) } if !strings.Contains(registration, "phone VARCHAR(32) PRIMARY KEY") { t.Fatalf("registration table should key by phone:\n%s", registration) } } func newMockDB(t *testing.T) (*sql.DB, sqlmock.Sqlmock) { t.Helper() db, mock, err := sqlmock.New() if err != nil { t.Fatalf("sqlmock.New() error = %v", err) } return db, mock }