package openplatform import ( "context" "regexp" "strings" "testing" "time" "github.com/DATA-DOG/go-sqlmock" ) func TestInQueryPreservesMySQLDateFormatPercentTokens(t *testing.T) { query, args := inQuery("SELECT DATE_FORMAT(stat_date,'%Y-%m-%d') FROM metrics WHERE stat_date=? AND vin IN (%s)", "2026-07-21", []string{"VIN1", "VIN2"}) if strings.Contains(query, "MISSING") || !strings.Contains(query, "DATE_FORMAT(stat_date,'%Y-%m-%d')") { t.Fatalf("date format was corrupted: %s", query) } if !strings.Contains(query, "vin IN (?,?)") { t.Fatalf("VIN placeholders missing: %s", query) } if len(args) != 3 || args[0] != "2026-07-21" || args[1] != "VIN1" || args[2] != "VIN2" { t.Fatalf("unexpected args: %#v", args) } } func TestAuthorizedVehiclesUsesBinaryVINJoin(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() start := time.Date(2026, 7, 20, 0, 0, 0, 0, time.Local) end := start.Add(24*time.Hour - time.Nanosecond) mock.ExpectQuery(regexp.QuoteMeta("JOIN vehicle_identity_binding b ON BINARY b.vin=BINARY g.vin")). WithArgs(uint64(1), start, end, "辽A00001"). WillReturnRows(sqlmock.NewRows([]string{"plate", "vin"}).AddRow("辽A00001", "LTEST000000000001")) repository := NewMySQLRepository(db) vehicles, err := repository.AuthorizedVehicles(context.Background(), 1, []string{"辽A00001"}, start, end) if err != nil { t.Fatal(err) } if vehicles["辽A00001"].VIN != "LTEST000000000001" { t.Fatalf("unexpected vehicles: %#v", vehicles) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestAuthorizedVehiclesWithoutPlateFilterReturnsAllGrantedVehicles(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() start := time.Date(2026, 7, 20, 0, 0, 0, 0, time.Local) end := start.Add(24 * time.Hour) mock.ExpectQuery("JOIN vehicle_identity_binding.*TRIM.*ORDER BY UPPER"). WithArgs(uint64(7), start, end). WillReturnRows(sqlmock.NewRows([]string{"plate", "vin"}). AddRow("粤A12345", "LTEST32960VIN0001"). AddRow("粤B67890", "LTEST32960VIN0002")) vehicles, err := NewMySQLRepository(db).AuthorizedVehicles(context.Background(), 7, nil, start, end) if err != nil { t.Fatal(err) } if len(vehicles) != 2 || vehicles["粤A12345"].VIN != "LTEST32960VIN0001" || vehicles["粤B67890"].VIN != "LTEST32960VIN0002" { t.Fatalf("vehicles=%#v", vehicles) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestDailyMileageReturnsDailyAndSameProtocolEndTotal(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() mock.ExpectQuery("SELECT MAX\\(selected.latest_event_time\\).*FROM vehicle_daily_mileage m\\s+WHERE m.stat_date BETWEEN \\? AND \\?.*m.latest_total_mileage_km>=0.*m.daily_mileage_km>=0"). WithArgs("2026-07-21", "2026-07-21", "LTEST32960VIN0001", "LTEST32960VIN0002"). WillReturnRows(sqlmock.NewRows([]string{"vin", "date", "protocol", "daily_mileage_km", "latest_total_mileage_km", "data_time", "updated_at"}). AddRow("LTEST32960VIN0001", "2026-07-21", "GB32960", 101.235, 12345.679, "2026-07-21T23:58:45+08:00", "2026-07-22T05:10:00+08:00")) values, err := NewMySQLRepository(db).DailyMileage(context.Background(), []string{"LTEST32960VIN0001", "LTEST32960VIN0002"}, "2026-07-21", nil) if err != nil { t.Fatal(err) } first := values["LTEST32960VIN0001"] if first.MileageKm != 101.235 || first.TotalMileageKm != 12345.679 || first.DataTime != "2026-07-21T23:58:45+08:00" || first.UpdatedAt == "" { t.Fatalf("first=%#v", first) } if _, ok := values["LTEST32960VIN0002"]; ok { t.Fatalf("vehicle without a cumulative total must not be returned: %#v", values) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestDailyMileageExplicitPriorityFiltersDisabledProtocolsAndKeepsZero(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() mock.ExpectQuery("m.protocol IN \\(\\?,\\?\\).*ORDER BY m.stat_date,m.vin,CASE m.protocol WHEN \\? THEN 1 WHEN \\? THEN 2"). WithArgs("2026-07-21", "2026-07-21", "LTEST32960VIN0001", "JT808", "GB32960", "JT808", "GB32960"). WillReturnRows(sqlmock.NewRows([]string{"vin", "date", "protocol", "daily_mileage_km", "latest_total_mileage_km", "data_time", "updated_at"}). AddRow("LTEST32960VIN0001", "2026-07-21", "JT808", 0.0, 12000.0, "2026-07-21T23:58:45+08:00", "2026-07-22T05:10:00+08:00"). AddRow("LTEST32960VIN0001", "2026-07-21", "GB32960", 12.0, 12012.0, "2026-07-21T23:59:00+08:00", "2026-07-22T05:10:00+08:00")) values, err := NewMySQLRepository(db).DailyMileage(context.Background(), []string{"LTEST32960VIN0001"}, "2026-07-21", []string{"JT808", "GB32960"}) if err != nil { t.Fatal(err) } selected := values["LTEST32960VIN0001"] if selected.Protocol != "JT808" || selected.MileageKm != 0 { t.Fatalf("selected=%#v", selected) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestLatestMileageBeforeUsesRequestedProtocolPriority(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() mock.ExpectQuery("SELECT MAX\\(selected.latest_event_time\\).*SELECT prior.vin,prior.protocol,MAX\\(prior.stat_date\\).*FROM vehicle_daily_mileage prior\\s+WHERE prior.stat_date<\\?.*prior.protocol IN \\(\\?,\\?\\).*ORDER BY m.vin,CASE m.protocol"). WithArgs("2026-07-22", "LTEST32960VIN0001", "JT808", "YUTONG_MQTT", "JT808", "YUTONG_MQTT"). WillReturnRows(sqlmock.NewRows([]string{"vin", "date", "protocol", "daily_mileage_km", "latest_total_mileage_km", "data_time", "updated_at"}). AddRow("LTEST32960VIN0001", "2026-07-20", "JT808", 18.5, 9008.5, "2026-07-20T22:00:00+08:00", "2026-07-21T01:00:00+08:00"). AddRow("LTEST32960VIN0001", "2026-07-21", "YUTONG_MQTT", 20.0, 12020.0, "2026-07-21T23:00:00+08:00", "2026-07-22T01:00:00+08:00")) values, err := NewMySQLRepository(db).LatestMileageBefore(context.Background(), []string{"LTEST32960VIN0001"}, "2026-07-22", []string{"JT808", "YUTONG_MQTT"}) if err != nil { t.Fatal(err) } selected := values["LTEST32960VIN0001"] if selected.Protocol != "JT808" || selected.MileageKm != 18.5 || selected.TotalMileageKm != 9008.5 || selected.DataTime != "2026-07-20T22:00:00+08:00" || selected.UpdatedAt != "2026-07-21T01:00:00+08:00" { t.Fatalf("selected=%#v", selected) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestTotalMileageUsesProtocolPriorityAndLatestRecordAtOrBeforeTime(t *testing.T) { mysqlDB, _, err := sqlmock.New() if err != nil { t.Fatal(err) } defer mysqlDB.Close() tdDB, tdMock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer tdDB.Close() at := time.Date(2026, 7, 21, 9, 30, 0, 0, time.FixedZone("CST", 8*3600)) tdMock.ExpectQuery("protocol='GB32960'.*ts<='2026-07-21T09:30:00\\+08:00'.*ORDER BY ts DESC LIMIT 1"). WillReturnRows(sqlmock.NewRows([]string{"ts", "total_mileage_km", "protocol"})) tdMock.ExpectQuery("protocol='YUTONG_MQTT'.*ts<='2026-07-21T09:30:00\\+08:00'.*ORDER BY ts DESC LIMIT 1"). WillReturnRows(sqlmock.NewRows([]string{"ts", "total_mileage_km", "protocol"}).AddRow(at.Add(-15*time.Second).UnixMilli(), 12345.678, "YUTONG_MQTT")) repository := NewMySQLRepository(mysqlDB).WithTDengine(tdDB, "lingniu_vehicle_ts") point, err := repository.TotalMileage(context.Background(), "LA9GG68L2PBAF4790", at, []string{"GB32960", "YUTONG_MQTT", "JT808"}) if err != nil { t.Fatal(err) } if point == nil || point.Protocol != "YUTONG_MQTT" || point.TotalMileageKm != 12345.678 || !point.ObservedAt.Equal(at.Add(-15*time.Second)) { t.Fatalf("point=%#v", point) } if err := tdMock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestListVehicleGrantsUsesBinaryVINJoin(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() now := time.Date(2026, 7, 20, 0, 0, 0, 0, time.Local) mock.ExpectQuery(regexp.QuoteMeta("LEFT JOIN vehicle_identity_binding b ON BINARY b.vin=BINARY g.vin")). WithArgs(uint64(1)). WillReturnRows(sqlmock.NewRows([]string{"vin", "plate", "valid_from", "valid_to", "granted_by", "updated_at"}). AddRow("LTEST000000000001", "辽A00001", now, now.AddDate(1, 0, 0), "admin", now)) repository := NewMySQLRepository(db) grants, err := repository.ListVehicleGrants(context.Background(), 1) if err != nil { t.Fatal(err) } if len(grants) != 1 || grants[0].Plate != "辽A00001" { t.Fatalf("unexpected grants: %#v", grants) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }