121 lines
4.0 KiB
Go
121 lines
4.0 KiB
Go
package milergeo
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
// fakeRedis answers the three geo commands the way a given Redis would.
|
|
type fakeRedis struct {
|
|
searchErr error // GEOSEARCH result; nil = supported
|
|
radiusErr error
|
|
addErr error
|
|
locs []redis.GeoLocation
|
|
searchHits int
|
|
radiusHits int
|
|
}
|
|
|
|
func (f *fakeRedis) GeoSearchLocation(ctx context.Context, _ string, q *redis.GeoSearchLocationQuery) *redis.GeoSearchLocationCmd {
|
|
f.searchHits++
|
|
cmd := redis.NewGeoSearchLocationCmd(ctx, q)
|
|
if f.searchErr != nil {
|
|
cmd.SetErr(f.searchErr)
|
|
} else {
|
|
cmd.SetVal(f.locs)
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func (f *fakeRedis) GeoRadius(_ context.Context, _ string, _, _ float64, _ *redis.GeoRadiusQuery) *redis.GeoLocationCmd {
|
|
f.radiusHits++
|
|
return redis.NewGeoLocationCmdResult(f.locs, f.radiusErr)
|
|
}
|
|
|
|
func (f *fakeRedis) GeoAdd(ctx context.Context, _ string, _ ...*redis.GeoLocation) *redis.IntCmd {
|
|
cmd := redis.NewIntCmd(ctx)
|
|
if f.addErr != nil {
|
|
cmd.SetErr(f.addErr)
|
|
} else {
|
|
cmd.SetVal(1)
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
var nearby = []redis.GeoLocation{{Name: "38", Dist: 0.03}, {Name: "23", Dist: 0.4}}
|
|
|
|
func TestSearchUsesGeosearchOnRedis62(t *testing.T) {
|
|
legacyOnly.Store(false)
|
|
f := &fakeRedis{locs: nearby}
|
|
got, err := Search(context.Background(), f, 11.0053, 76.9511, 10, 10)
|
|
if err != nil || len(got) != 2 || got[0].Name != "38" {
|
|
t.Fatalf("got %v, %v", got, err)
|
|
}
|
|
if f.searchHits != 1 || f.radiusHits != 0 {
|
|
t.Fatalf("GEOSEARCH must be used when supported: search=%d radius=%d", f.searchHits, f.radiusHits)
|
|
}
|
|
}
|
|
|
|
// The production failure: Redis < 6.2 has no GEOSEARCH, so every order found
|
|
// "no riders". The fallback must find them, and stop retrying GEOSEARCH.
|
|
func TestSearchFallsBackOnOldRedis(t *testing.T) {
|
|
legacyOnly.Store(false)
|
|
t.Cleanup(func() { legacyOnly.Store(false) })
|
|
f := &fakeRedis{searchErr: errors.New("ERR unknown command 'GEOSEARCH', with args beginning with: 'milers:locations'"), locs: nearby}
|
|
for i := 0; i < 3; i++ {
|
|
got, err := Search(context.Background(), f, 11.0053, 76.9511, 10, 10)
|
|
if err != nil || len(got) != 2 {
|
|
t.Fatalf("call %d: got %v, %v", i, got, err)
|
|
}
|
|
}
|
|
if f.searchHits != 1 || f.radiusHits != 3 {
|
|
t.Fatalf("after the first rejection only GEORADIUS should run: search=%d radius=%d", f.searchHits, f.radiusHits)
|
|
}
|
|
if p := Probe(context.Background(), f); !strings.Contains(p, "GEORADIUS fallback") {
|
|
t.Fatalf("probe = %q", p)
|
|
}
|
|
}
|
|
|
|
// Any other failure must surface, not read as an empty street.
|
|
func TestSearchReturnsOtherErrors(t *testing.T) {
|
|
legacyOnly.Store(false)
|
|
f := &fakeRedis{searchErr: errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")}
|
|
if _, err := Search(context.Background(), f, 11, 76, 10, 10); err == nil || !strings.Contains(err.Error(), "WRONGTYPE") {
|
|
t.Fatalf("want the WRONGTYPE error, got %v", err)
|
|
}
|
|
if f.radiusHits != 0 || legacyOnly.Load() {
|
|
t.Fatal("a non-'unknown command' error must not switch to the fallback")
|
|
}
|
|
if p := Probe(context.Background(), f); !strings.HasPrefix(p, "error: ") {
|
|
t.Fatalf("probe = %q", p)
|
|
}
|
|
}
|
|
|
|
func TestIndexReportsWriteFailure(t *testing.T) {
|
|
if err := Index(context.Background(), &fakeRedis{}, 38, 11.0, 76.9); err != nil {
|
|
t.Fatalf("ok write: %v", err)
|
|
}
|
|
err := Index(context.Background(), &fakeRedis{addErr: errors.New("WRONGTYPE Operation")}, 38, 11.0, 76.9)
|
|
if err == nil || !strings.Contains(err.Error(), "WRONGTYPE") {
|
|
t.Fatalf("want the write error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// db.Rdb is a *redis.Client; before InitRedis it is a nil pointer, which in
|
|
// an interface is not == nil. It must be refused, not dereferenced.
|
|
func TestNilClientIsRefused(t *testing.T) {
|
|
var rdb *redis.Client
|
|
if _, err := Search(context.Background(), rdb, 11, 76, 10, 10); err == nil {
|
|
t.Fatal("nil client must be an error")
|
|
}
|
|
if err := Index(context.Background(), rdb, 1, 11, 76); err == nil {
|
|
t.Fatal("nil client must be an error")
|
|
}
|
|
if p := Probe(context.Background(), rdb); !strings.HasPrefix(p, "error") {
|
|
t.Fatalf("probe = %q", p)
|
|
}
|
|
}
|