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 TestHydrogenStationsUsesNewMasterAndExcelSinopecRoster(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() mock.ExpectQuery("cooperation_start_date.*FROM ln_asset_management\\.new_hydrogen_site n.*common_district province_region.*common_district city_region.*ORDER BY province_region\\.NAME,city_region\\.NAME,n\\.site_name,n\\.id"). WithArgs("湖北省", "湖北省", "武汉市", "武汉市"). WillReturnRows(sqlmock.NewRows([]string{ "id", "name", "short_name", "address", "longitude", "latitude", "province", "city", "district", "cooperative", "contact_person", "contact_phone", "unit_price", "monthly_hydrogen_kg", "total_hydrogen_kg", }). AddRow("1", "武汉群力加油站", "中国石化群力加油站", "地址一", 114.1, 30.2, "湖北省", "武汉市", "", false, "张工", "13800000001", 12.5, 12.5, 100.5). AddRow("2", "已有合作日期站", "", "地址二", 120.1, 31.2, "上海市", "上海市", "", true, "李工", "13800000002", 13.0, 8.5, 80.5). AddRow("3", "普通外部站", "", "地址三", 121.1, 32.2, "江苏省", "南京市", "", false, "", "", 0, 0, 0)) cooperateOnly := true stations, err := NewMySQLRepository(db).HydrogenStations(context.Background(), HydrogenStationRequest{Province: "湖北省", City: "武汉市", CooperateOnly: &cooperateOnly}) if err != nil { t.Fatal(err) } if len(stations) != 2 { t.Fatalf("expected roster and canonical cooperative stations, got %#v", stations) } if stations[0].Name != "武汉群力加油站" || !stations[0].Cooperative { t.Fatalf("Excel SINOPEC station was not classified as cooperative: %#v", stations[0]) } if stations[1].Name != "已有合作日期站" || !stations[1].Cooperative { t.Fatalf("canonical cooperative station was lost: %#v", stations[1]) } if stations[0].ContactPerson != "张工" || stations[0].ContactPhone != "13800000001" || stations[0].UnitPrice != 12.5 { t.Fatalf("partner station contact and price were not mapped: %#v", stations[0]) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestExcelSinopecRosterMatchesEitherExcelNameColumn(t *testing.T) { for _, name := range []string{"中国石化青岛炼油化工有限责任公司", "青岛中石化加氢站", "南京溧水柘塘东站", "南京中石化溧水加氢站", "中国石化樟坑加油加氢站"} { if !isExcelSinopecStation(name) { t.Fatalf("expected roster match for %q", name) } } if isExcelSinopecStation("普通外部加氢站") { t.Fatal("non-roster station must not be classified as cooperative") } } func TestDailyMileageReturnsDailyAndAuthoritativeSameProtocolEndTotal(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() mock.ExpectQuery("COALESCE\\(m.day_end_total_mileage_km,m.latest_total_mileage_km\\).*FROM vehicle_daily_mileage m\\s+WHERE m.stat_date BETWEEN \\? AND \\?.*COALESCE\\(m.day_end_total_mileage_km,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 TestStationaryLocationPointsUsesTimeVINAndSmallBoundingBox(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() start := time.Date(2026, 8, 6, 10, 0, 0, 0, time.FixedZone("CST", 8*3600)) tdMock.ExpectQuery("FROM lingniu_vehicle_ts.vehicle_locations.*ts>='2026-08-06T10:00:00\\+08:00'.*vin IN \\('LTEST32960VIN0001'\\).*speed_kmh BETWEEN 0 AND 3\\.000.*ORDER BY vin,ts ASC,protocol ASC"). WillReturnRows(sqlmock.NewRows([]string{"ts", "vin", "protocol", "longitude", "latitude", "speed_kmh"}). AddRow(start.UnixMilli(), "LTEST32960VIN0001", "GB32960", 120.000001, 30.000001, 0.2)) points, err := NewMySQLRepository(mysqlDB).WithTDengine(tdDB, "lingniu_vehicle_ts").StationaryLocationPoints(context.Background(), []string{"LTEST32960VIN0001"}, start, start.Add(time.Hour), 120, 30, 5, 3) if err != nil || len(points) != 1 || points[0].SpeedKmh != 0.2 || !points[0].ObservedAt.Equal(start) { t.Fatalf("points=%#v err=%v", points, err) } if err := tdMock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestRealtimeVehiclesAnyFreshProtocolKeepsSelectedSourceOnline(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() now := time.Date(2026, 8, 3, 21, 37, 30, 0, time.Local) dayStart := time.Date(2026, 8, 3, 0, 0, 0, 0, time.Local) vin := "LTEST32960VIN0001" mock.ExpectQuery("SELECT l.vin,l.protocol.*FROM vehicle_realtime_location"). WithArgs(dayStart, vin, now.Add(-10*time.Minute)). WillReturnRows(sqlmock.NewRows([]string{"vin", "protocol", "longitude", "latitude", "speed_kmh", "soc_percent", "total_mileage_km", "updated_at", "active_today"}). AddRow(vin, "GB32960", 120.1, 30.2, 0, 86.5, 1000, now.Add(-2*time.Minute), true). AddRow(vin, "JT808", 120.2, 30.3, 10, nil, 0, now.Add(-20*time.Second), true)) points, err := NewMySQLRepository(db).RealtimeVehicles(context.Background(), []string{vin}, now) if err != nil { t.Fatal(err) } point := points[vin] if point.Protocol != "GB32960" || point.SOCPercent == nil || *point.SOCPercent != 86.5 || !point.Online || !point.ActiveToday || !point.ObservedAt.Equal(now.Add(-2*time.Minute)) { t.Fatalf("selected source and aggregate online state mismatch: %#v", point) } if err := mock.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) } }