Files
lingniu-vehicle-ingest/go/vehicle-gateway/internal/identity/snapshot_test.go

167 lines
5.9 KiB
Go

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)
}
}