Files
lingniu-vehicle-ingest/go/vehicle-gateway/internal/identity/resolver_test.go
2026-07-03 08:07:32 +08:00

280 lines
8.6 KiB
Go

package identity
import (
"context"
"database/sql"
"strings"
"testing"
"time"
"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 TestMySQLResolverLooksUpVINByUniqueKeyWithoutSort(t *testing.T) {
db, mock := newMockDB(t)
defer db.Close()
mock.ExpectQuery("SELECT vin FROM vehicle_identity_binding WHERE phone = \\? AND vin IS NOT NULL AND vin <> ''$").
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)
}
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))
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 TestMySQLResolverCachesIdentityMissesForHighFrequencyFrames(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)
resolver := NewMySQLResolverWithOptions(db, "vehicle_identity_binding", MySQLResolverOptions{
LocationTouchInterval: time.Hour,
LookupCacheTTL: time.Hour,
})
for i := 0; i < 2; i++ {
_, 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 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
}