package identity import ( "context" "errors" "testing" "time" "github.com/DATA-DOG/go-sqlmock" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) func TestRefreshSnapshotResolvesKnownJT808WithoutPerFrameQueries(t *testing.T) { db, mock := newMockDB(t) defer db.Close() expectIdentitySnapshot(mock) resolver := NewMySQLResolverWithOptions(db, "vehicle_identity_binding", MySQLResolverOptions{ SnapshotOnlyLookups: true, }) result, err := resolver.RefreshSnapshot(context.Background()) if err != nil { t.Fatalf("RefreshSnapshot() error = %v", err) } if result.BindingEntries != 3 || result.IdentifierEntries != 2 || result.RegistrationEntries != 1 || result.SourceEntries != 1 { t.Fatalf("snapshot result = %+v", result) } resolved, err := resolver.Resolve(context.Background(), envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, Phone: "013307795425", SourceEndpoint: "115.231.168.135:43625", Parsed: map[string]any{}, }) if err != nil { t.Fatalf("Resolve() error = %v", err) } if resolved.VIN != "LNBVIN00000000001" { t.Fatalf("vin = %q", resolved.VIN) } if resolved.SourceCode != "g7s" || resolved.PlatformName != "G7s" || resolved.SourceKind != "PLATFORM" { t.Fatalf("source metadata = code:%q platform:%q kind:%q", resolved.SourceCode, resolved.PlatformName, resolved.SourceKind) } identityMetadata, _ := resolved.Parsed["identity"].(map[string]any) if identityMetadata["cache_status"] != "snapshot" { t.Fatalf("identity metadata = %#v, want snapshot cache status", identityMetadata) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestSnapshotOnlyResolverMissDoesNotQueryMySQL(t *testing.T) { db, mock := newMockDB(t) defer db.Close() resolver := NewMySQLResolverWithOptions(db, "vehicle_identity_binding", MySQLResolverOptions{ SnapshotOnlyLookups: true, }) resolved, err := resolver.Resolve(context.Background(), envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, Phone: "013307700000", Parsed: map[string]any{}, }) if err != nil { t.Fatalf("Resolve() error = %v", err) } if resolved.VIN != "" { t.Fatalf("vin = %q, want unresolved", resolved.VIN) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("snapshot-only miss should not query mysql: %v", err) } } func TestRefreshSnapshotFailureKeepsLastKnownGoodSnapshot(t *testing.T) { db, mock := newMockDB(t) defer db.Close() expectIdentitySnapshot(mock) resolver := NewMySQLResolverWithOptions(db, "vehicle_identity_binding", MySQLResolverOptions{ SnapshotOnlyLookups: true, }) first, err := resolver.RefreshSnapshot(context.Background()) if err != nil { t.Fatalf("first RefreshSnapshot() error = %v", err) } mock.ExpectQuery("SELECT vin, plate, phone"). WillReturnError(errors.New("mysql unavailable")) if _, err := resolver.RefreshSnapshot(context.Background()); err == nil { t.Fatal("second RefreshSnapshot() error = nil, want failure") } resolved, err := resolver.Resolve(context.Background(), envelope.FrameEnvelope{ Protocol: envelope.ProtocolJT808, Phone: "013307795425", Parsed: map[string]any{}, }) if err != nil { t.Fatalf("Resolve() after failed refresh error = %v", err) } if resolved.VIN != "LNBVIN00000000001" { t.Fatalf("vin after failed refresh = %q", resolved.VIN) } stats := resolver.CacheStats() if !stats.SnapshotReady || stats.SnapshotRefreshedAt.IsZero() || !stats.SnapshotRefreshedAt.Equal(first.RefreshedAt) { t.Fatalf("snapshot stats after failed refresh = %+v, first = %+v", stats, first) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func expectIdentitySnapshot(mock sqlmock.Sqlmock) { mock.ExpectQuery("SELECT vin, plate, phone"). WillReturnRows(sqlmock.NewRows([]string{"vin", "plate", "phone"}). AddRow("LNBVIN00000000001", "粤A00001", "13307795425")) mock.ExpectQuery("SELECT protocol, source_code, identifier_type, identifier_value"). WillReturnRows(sqlmock.NewRows([]string{"protocol", "source_code", "identifier_type", "identifier_value", "vin", "platform_name"}). AddRow("JT808", "g7s", "JT808_PHONE", "13307795425", "LNBVIN00000000001", "G7s")) mock.ExpectQuery("SELECT phone, vin, device_id, plate, auth_token"). WillReturnRows(sqlmock.NewRows([]string{"phone", "vin", "device_id", "plate", "auth_token"}). AddRow("13307795425", "LNBVIN00000000001", "DEVICE-1", "粤A00001", "device-code")) mock.ExpectQuery("SELECT protocol, source_ip, source_code, platform_name, source_kind"). WillReturnRows(sqlmock.NewRows([]string{"protocol", "source_ip", "source_code", "platform_name", "source_kind"}). AddRow("JT808", "115.231.168.135", "g7s", "G7s", "PLATFORM")) } func TestSnapshotServesJT808AuthenticationTokenByNormalizedPhone(t *testing.T) { db, mock := newMockDB(t) defer db.Close() expectIdentitySnapshot(mock) resolver := NewMySQLResolverWithOptions(db, "vehicle_identity_binding", MySQLResolverOptions{ SnapshotOnlyLookups: true, }) if _, err := resolver.RefreshSnapshot(context.Background()); err != nil { t.Fatalf("RefreshSnapshot() error = %v", err) } token, ok := resolver.JT808AuthToken("0013307795425") if !ok || token != "device-code" { t.Fatalf("JT808AuthToken() = %q, %v", token, ok) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestSnapshotResultRefreshedAtUsesCurrentTime(t *testing.T) { db, mock := newMockDB(t) defer db.Close() expectIdentitySnapshot(mock) resolver := NewMySQLResolverWithOptions(db, "vehicle_identity_binding", MySQLResolverOptions{ SnapshotOnlyLookups: true, }) before := time.Now() result, err := resolver.RefreshSnapshot(context.Background()) if err != nil { t.Fatalf("RefreshSnapshot() error = %v", err) } if result.RefreshedAt.Before(before) || result.RefreshedAt.After(time.Now()) { t.Fatalf("refreshed_at = %v, want current time", result.RefreshedAt) } }