package main import ( "context" "fmt" "math" "testing" "time" "github.com/DATA-DOG/go-sqlmock" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) func TestDailySourceEndUsesVehicleEventTimeAfterLateDelivery(t *testing.T) { for _, tc := range []struct { vin, phone, ip, code string lateHour, lateMin int endHour, endMin int lateKM, endKM float64 }{ {"LGAX2BG42H1003112", "40409727033", "122.152.221.156", "dongfang_beidou", 8, 59, 23, 59, 44478.4, 44402.1}, {"LA9HE60A0PBAF4002", "13307765803", "115.231.168.135", "xinda", 20, 46, 21, 14, 6194.8, 6195.1}, } { t.Run(tc.vin, func(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() db.SetMaxOpenConns(1) loc := time.FixedZone("Asia/Shanghai", 8*3600) first := time.Date(2026, 10, 21, 0, 0, 0, 0, loc) late := time.Date(2026, 10, 21, tc.lateHour, tc.lateMin, 0, 0, loc) last := time.Date(2026, 10, 21, tc.endHour, tc.endMin, 0, 0, loc) parsed := func(km float64) string { return fmt.Sprintf(`{"jt808.location.total_mileage_km":%f}`, km) } mock.ExpectQuery("(?s)SELECT vin.*MIN\\(CAST\\(event_time AS BIGINT\\)\\).*MAX\\(CAST\\(event_time AS BIGINT\\)\\)"). WillReturnRows(sqlmock.NewRows([]string{"vin", "phone", "device", "endpoint", "first", "first_parsed", "first_raw", "last", "last_parsed", "last_raw", "count", "minimum", "maximum"}). AddRow(tc.vin, tc.phone, "", tc.ip+":1234", first, parsed(tc.endKM-100), "", late, parsed(tc.lateKM), "", 100, first.UnixMilli(), last.UnixMilli())).RowsWillBeClosed() mock.ExpectQuery(fmt.Sprintf("(?s)SELECT event_time, parsed_json, raw_text.*phone = '%s'.*CAST\\(event_time AS BIGINT\\) <= %d.*ORDER BY event_time DESC, ts DESC", tc.phone, last.UnixMilli())). WillReturnRows(sqlmock.NewRows([]string{"event_time", "parsed_json", "raw_text"}).AddRow(last, parsed(tc.endKM), "")) cfg := config{TDengineDatabase: "lingniu_vehicle_ts", Location: loc} result, err := queryDailyLastSourceRows(context.Background(), db, cfg, envelope.ProtocolJT808, "2026-10-21") if err != nil { t.Fatal(err) } got := result[tc.vin][0] if !got.TS.Equal(last) || math.Abs(got.TotalKM-tc.endKM) > 0.0001 || got.RawSampleCount != 100 { t.Fatalf("late delivery replaced the vehicle day end: %+v", got) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }) } } func TestDailySourceFirstAlsoUsesVehicleEventTime(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() loc := time.FixedZone("Asia/Shanghai", 8*3600) first := time.Date(2026, 10, 21, 8, 0, 0, 0, loc) storageFirst := first.Add(time.Hour) last := first.Add(10 * time.Hour) mock.ExpectQuery("(?s)SELECT vin.*MIN\\(CAST").WillReturnRows(sqlmock.NewRows([]string{"vin", "phone", "device", "endpoint", "first", "first_parsed", "first_raw", "last", "last_parsed", "last_raw", "count", "minimum", "maximum"}). AddRow("VIN", "phone", "", "1.2.3.4:123", storageFirst, `{"jt808.location.total_mileage_km":101}`, "", last, `{"jt808.location.total_mileage_km":110}`, "", 5, first.UnixMilli(), last.UnixMilli())) mock.ExpectQuery(fmt.Sprintf("(?s)SELECT event_time.*CAST\\(event_time AS BIGINT\\) >= %d", first.UnixMilli())).WillReturnRows(sqlmock.NewRows([]string{"event", "parsed", "raw"}).AddRow(first, `{"jt808.location.total_mileage_km":100}`, "")) got, err := queryDailyLastSourceRows(context.Background(), db, config{TDengineDatabase: "db", Location: loc}, envelope.ProtocolJT808, "2026-10-21") if err != nil { t.Fatal(err) } if got["VIN"][0].FirstTotalKM != 100 || !got["VIN"][0].FirstTS.Equal(first) { t.Fatalf("wrong first event: %+v", got) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestMissingVehicleEventBoundaryFailsBeforeWriting(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() stamp := time.Date(2026, 10, 20, 20, 0, 0, 0, time.UTC) mock.ExpectQuery("(?s)SELECT vin.*MAX\\(CAST").WillReturnRows(sqlmock.NewRows([]string{"vin", "phone", "device", "endpoint", "last", "parsed", "raw", "count", "maximum"}).AddRow("VIN", "phone", "", "1.2.3.4:123", stamp, `{"jt808.location.total_mileage_km":100}`, "", 2, stamp.Add(time.Hour).UnixMilli())) mock.ExpectQuery("SELECT event_time").WillReturnRows(sqlmock.NewRows([]string{"event", "parsed", "raw"})) if _, err := queryPreviousLastSourceRows(context.Background(), db, config{TDengineDatabase: "db", Location: time.UTC}, envelope.ProtocolJT808, "2026-10-21"); err == nil { t.Fatal("missing event boundary silently accepted") } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestZeroMileageTailDoesNotHideLastUsableVehicleEvent(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() loc := time.FixedZone("Asia/Shanghai", 8*3600) first := time.Date(2026, 10, 21, 8, 0, 0, 0, loc) last := first.Add(time.Hour) zero := last.Add(time.Hour) mock.ExpectQuery("(?s)SELECT vin.*MIN\\(CAST").WillReturnRows(sqlmock.NewRows([]string{"vin", "phone", "device", "endpoint", "first", "first_parsed", "first_raw", "last", "last_parsed", "last_raw", "count", "minimum", "maximum"}).AddRow("VIN", "phone", "", "1.2.3.4:123", first, `{"jt808.location.total_mileage_km":100}`, "", zero, `{"jt808.location.total_mileage_km":0}`, "", 3, first.UnixMilli(), zero.UnixMilli())) mock.ExpectQuery("(?s)SELECT event_time.*ORDER BY event_time DESC, ts DESC").WillReturnRows(sqlmock.NewRows([]string{"event", "parsed", "raw"}).AddRow(zero, `{"jt808.location.total_mileage_km":0}`, "").AddRow(last, `{"jt808.location.total_mileage_km":110}`, "")) got, err := queryDailyLastSourceRows(context.Background(), db, config{TDengineDatabase: "db", Location: loc}, envelope.ProtocolJT808, "2026-10-21") if err != nil { t.Fatal(err) } if got["VIN"][0].TotalKM != 110 || !got["VIN"][0].TS.Equal(last) { t.Fatalf("non-positive frame replaced usable end: %+v", got) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }