package app import ( "encoding/json" "fmt" "testing" ) // Pull to refresh on 附近 should bring different faces without sending the // reader across town. Before this, a seed replaced the distance ordering // entirely and the list became a plain shuffle of the whole city. func TestNearbyShuffleStaysWithinDistanceBandsMySQL(t *testing.T) { db := isolatedIMDatabase(t) a := &App{db: db, hub: NewHub(), config: Config{Environment: "development", JWTSecret: "isolated-im-regression-secret-only"}} // Viewer at the city centre; then three people ~200m away and three ~40km away. const centreLat, centreLng = 22.5431, 114.0579 if _, err := db.Exec(`INSERT INTO user_location_states(user_id,city_code,latitude,longitude) VALUES(1,'440300',?,?)`, centreLat, centreLng); err != nil { t.Fatal(err) } near := map[int64]bool{} for id := int64(2); id <= 7; id++ { if id > 3 { if _, err := db.Exec(`INSERT INTO users(id,public_id,password_hash) VALUES(?,CONCAT('NEARBY',?),'!NO_LOGIN')`, id, id); err != nil { t.Fatal(err) } for _, statement := range []string{ `INSERT INTO user_profiles(user_id,nickname) VALUES(?,CONCAT('附近',?))`, `INSERT INTO user_privacy_settings(user_id) VALUES(?)`, } { args := []any{id} if statement == `INSERT INTO user_profiles(user_id,nickname) VALUES(?,CONCAT('附近',?))` { args = append(args, id) } if _, err := db.Exec(statement, args...); err != nil { t.Fatal(err) } } } offset := 0.002 * float64(id) // ~200m steps if id >= 5 { offset = 0.36 * float64(id) // ~40km and beyond } else { near[id] = true } if _, err := db.Exec(`INSERT INTO user_location_states(user_id,city_code,latitude,longitude) VALUES(?,'440300',?,?)`, id, centreLat+offset, centreLng); err != nil { t.Fatal(err) } } page := func(seed int) []int64 { t.Helper() var payload struct { Items []struct { ID int64 `json:"id"` } `json:"items"` } query := "/api/v1/nearby/users?page=1&pageSize=20" if seed > 0 { query += fmt.Sprintf("&seed=%d", seed) } if err := json.Unmarshal(imTestCall(t, a.nearby, 1, "GET", query, "", 200), &payload); err != nil { t.Fatal(err) } ids := []int64{} for _, item := range payload.Items { ids = append(ids, item.ID) } return ids } t.Run("the near band always comes first, whatever the seed", func(t *testing.T) { for _, seed := range []int{0, 7, 12345, 2147483647} { ids := page(seed) if len(ids) < 6 { t.Fatalf("seed %d 返回 %v", seed, ids) } for index, id := range ids[:3] { if !near[id] { t.Fatalf("seed %d 第 %d 位是远处的 %d: %v", seed, index+1, id, ids) } } for _, id := range ids[3:] { if near[id] { t.Fatalf("seed %d 把近处的人排到了后面: %v", seed, ids) } } } }) t.Run("different seeds reorder people inside the band", func(t *testing.T) { seen := map[string]bool{} for seed := 1; seed <= 40; seed++ { ids := page(seed) seen[fmt.Sprint(ids[:3])] = true } if len(seen) < 2 { t.Fatalf("刷新应当换顺序,实际只有一种排列: %v", seen) } }) t.Run("one seed is stable, so paging cannot repeat or skip a row", func(t *testing.T) { first := page(99) for attempt := 0; attempt < 3; attempt++ { if fmt.Sprint(page(99)) != fmt.Sprint(first) { t.Fatalf("同一 seed 必须给出同一顺序: %v vs %v", page(99), first) } } }) }