Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/api/internal/dailygeo/worker_test.go
T

139 lines
5.9 KiB
Go

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