package dailygeo import ( "context" "database/sql" "errors" "github.com/DATA-DOG/go-sqlmock" "net/http" "net/http/httptest" "regexp" "strings" "testing" "time" ) type resolverFunc func(context.Context, float64, float64) (Address, error) func (f resolverFunc) Resolve(c context.Context, x, y float64) (Address, error) { return f(c, x, y) } func TestAMapMunicipalityAndRegion(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("key") != "test-key" { t.Error("missing credential") } w.Write([]byte(`{"status":"1","regeocode":{"addressComponent":{"province":"上海市","city":[],"adcode":"310101"}}}`)) })) defer server.Close() a, e := (&AMap{Key: "test-key", BaseURL: server.URL}).Resolve(context.Background(), 121.47, 31.23) if e != nil || a.Province != "上海市" || a.City != "上海市" || a.Region != "华东" { t.Fatalf("address=%+v error=%v", a, e) } for p, want := range map[string]string{"广东省": "华南", "四川省": "西南", "陕西省": "西北", "山西省": "华北", "湖北省": "华中", "吉林省": "东北", "": ""} { if Region(p) != want { t.Fatal(p) } } } func TestAMapQuotaFailureDoesNotLeakKey(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"status":"0","infocode":"10021"}`)) })) defer server.Close() _, e := (&AMap{Key: "secret-value", BaseURL: server.URL}).Resolve(context.Background(), 121, 31) if e == nil || !strings.Contains(e.Error(), "10021") || strings.Contains(e.Error(), "secret-value") { t.Fatal(e) } } func TestCachedCoordinateNeedsNoGeocoding(t *testing.T) { db, m, e := sqlmock.New() if e != nil { t.Fatal(e) } defer db.Close() m.ExpectQuery("SELECT province,city,region,adcode").WithArgs(121.0, 31.0).WillReturnRows(sqlmock.NewRows([]string{"province", "city", "region", "adcode"}).AddRow("上海市", "上海市", "华东", "310000")) m.ExpectExec("UPDATE vehicle_daily_geography SET province=.*WHERE vin=\\? AND stat_date=\\? AND longitude=\\? AND latitude=\\?").WithArgs("上海市", "上海市", "华东", "310000", "VIN1", "2026-08-31", 121.0, 31.0).WillReturnResult(sqlmock.NewResult(0, 1)) w := New(db, nil, "", resolverFunc(func(context.Context, float64, float64) (Address, error) { t.Fatal("cache hit must not call external API") return Address{}, nil })) if e = w.resolveOne(context.Background(), pending{"VIN1", "2026-08-31", 121, 31}); e != nil { t.Fatal(e) } if e = m.ExpectationsWereMet(); e != nil { t.Fatal(e) } } func TestResolverFailurePersistsRetryInsteadOfFalseUnknown(t *testing.T) { db, m, _ := sqlmock.New() defer db.Close() m.ExpectQuery("SELECT province,city,region,adcode").WillReturnError(sql.ErrNoRows) m.ExpectExec("UPDATE vehicle_daily_geography SET status='ERROR'.*next_attempt_at=.*WHERE vin=\\? AND stat_date=\\? AND longitude=\\? AND latitude=\\?").WithArgs("quota", "VIN1", "2026-08-31", 121.0, 31.0).WillReturnResult(sqlmock.NewResult(0, 1)) w := New(db, nil, "", resolverFunc(func(context.Context, float64, float64) (Address, error) { return Address{}, errors.New("quota") })) if e := w.resolveOne(context.Background(), pending{"VIN1", "2026-08-31", 121, 31}); e == nil { t.Fatal("expected failure") } if e := m.ExpectationsWereMet(); e != nil { t.Fatal(e) } } func TestHistoricalFailureDoesNotMarkMissingLocation(t *testing.T) { db, m, _ := sqlmock.New() defer db.Close() td, tm, _ := sqlmock.New() defer td.Close() tm.ExpectQuery("SELECT vin,protocol,LAST").WillReturnError(errors.New("historical store unavailable")) w := New(db, td, "test_ts", nil) if _, e := w.RefreshDay(context.Background(), "2026-08-31"); e == nil { t.Fatal("expected history error") } if e := m.ExpectationsWereMet(); e != nil { t.Fatal(e) } if e := tm.ExpectationsWereMet(); e != nil { t.Fatal(e) } } func TestHistoryUsesLatestSourceInsideBusinessDate(t *testing.T) { db, m, _ := sqlmock.New() defer db.Close() td, tm, _ := sqlmock.New() defer td.Close() early := time.Date(2026, 8, 31, 2, 0, 0, 0, Shanghai) late := early.Add(20 * time.Hour) tm.ExpectQuery("SELECT vin,protocol,LAST.*2026-08-31T00:00:00\\+08:00.*2026-09-01T00:00:00\\+08:00.*PARTITION BY vin,protocol").WillReturnRows(sqlmock.NewRows([]string{"vin", "protocol", "ts", "longitude", "latitude"}).AddRow("VIN1", "GB32960", late, 121.0, 31.0).AddRow("VIN1", "JT808", early, 113.0, 23.0).AddRow("VIN2", "JT808", late.Add(5*time.Hour), 110.0, 20.0)) m.ExpectExec("UPDATE vehicle_daily_geography SET longitude=NULL").WithArgs("2026-08-31").WillReturnResult(sqlmock.NewResult(0, 1)) m.ExpectExec(regexp.QuoteMeta(savePointSQL)).WithArgs("VIN1", "2026-08-31", 121.0, 31.0, late, "GB32960").WillReturnResult(sqlmock.NewResult(0, 1)) m.ExpectExec("INSERT IGNORE INTO vehicle_daily_geography").WithArgs("2026-08-31").WillReturnResult(sqlmock.NewResult(0, 0)) w := New(db, td, "test_ts", nil) n, e := w.RefreshDay(context.Background(), "2026-08-31") if e != nil || n != 1 { t.Fatalf("n=%d error=%v", n, e) } if e = m.ExpectationsWereMet(); e != nil { t.Fatal(e) } if e = tm.ExpectationsWereMet(); e != nil { t.Fatal(e) } } func TestProvinceAdministeredCity(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"status":"1","regeocode":{"addressComponent":{"province":"湖北省","city":[],"district":"潜江市","adcode":"429005"}}}`)) })) defer server.Close() a, e := (&AMap{Key: "test", BaseURL: server.URL}).Resolve(context.Background(), 112.807848, 30.381828) if e != nil || a.City != "潜江市" || a.Region != "华中" { t.Fatalf("address=%+v error=%v", a, e) } } func TestInvalidLocationSentinel(t *testing.T) { p := Point{VIN: "VIN1", Time: time.Now(), Longitude: -0.999999, Latitude: -0.999999} if validPoint(p) { t.Fatal("invalid device sentinel accepted") } p.Longitude, p.Latitude = 114.3, 30.5 if !validPoint(p) { t.Fatal("valid location rejected") } }