mirror of
https://github.com/kevin-DL/services.git
synced 2026-01-13 11:35:26 +00:00
534 lines
15 KiB
Go
534 lines
15 KiB
Go
package handler_test
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang/protobuf/ptypes/timestamp"
|
|
"github.com/google/uuid"
|
|
geo "github.com/hailocab/go-geoindex"
|
|
"github.com/stretchr/testify/assert"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
"google.golang.org/protobuf/types/known/wrapperspb"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/micro/services/places/handler"
|
|
"github.com/micro/services/places/model"
|
|
pb "github.com/micro/services/places/proto"
|
|
)
|
|
|
|
func testHandler(t *testing.T) pb.PlacesHandler {
|
|
// connect to the database
|
|
addr := os.Getenv("POSTGRES_URL")
|
|
if len(addr) == 0 {
|
|
addr = "postgresql://postgres@localhost:5432/postgres?sslmode=disable"
|
|
}
|
|
db, err := gorm.Open(postgres.Open(addr), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("Error connecting to database: %v", err)
|
|
}
|
|
|
|
// clean any data from a previous run
|
|
if err := db.Exec("DROP TABLE IF EXISTS locations CASCADE").Error; err != nil {
|
|
t.Fatalf("Error cleaning database: %v", err)
|
|
}
|
|
|
|
// migrate the database
|
|
if err := db.AutoMigrate(&model.Location{}); err != nil {
|
|
t.Fatalf("Error migrating database: %v", err)
|
|
}
|
|
|
|
return &handler.Places{DB: db, Geoindex: geo.NewPointsIndex(geo.Km(0.1))}
|
|
}
|
|
|
|
func TestSave(t *testing.T) {
|
|
tt := []struct {
|
|
Name string
|
|
Places []*pb.Location
|
|
Error error
|
|
}{
|
|
{
|
|
Name: "NoPlaces",
|
|
Error: handler.ErrMissingPlaces,
|
|
},
|
|
{
|
|
Name: "NoLatitude",
|
|
Places: []*pb.Location{
|
|
{
|
|
Longitude: &wrapperspb.DoubleValue{Value: -0.1246},
|
|
Id: uuid.New().String(),
|
|
},
|
|
},
|
|
Error: handler.ErrMissingLatitude,
|
|
},
|
|
{
|
|
Name: "NoLongitude",
|
|
Places: []*pb.Location{
|
|
{
|
|
Latitude: &wrapperspb.DoubleValue{Value: -0.1246},
|
|
Id: uuid.New().String(),
|
|
},
|
|
},
|
|
Error: handler.ErrMissingLongitude,
|
|
},
|
|
{
|
|
Name: "OneLocation",
|
|
Places: []*pb.Location{
|
|
{
|
|
Latitude: &wrapperspb.DoubleValue{Value: 51.5007},
|
|
Longitude: &wrapperspb.DoubleValue{Value: 0.1246},
|
|
Timestamp: timestamppb.New(time.Now()),
|
|
Id: uuid.New().String(),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "ManyPlaces",
|
|
Places: []*pb.Location{
|
|
{
|
|
Latitude: &wrapperspb.DoubleValue{Value: 51.5007},
|
|
Longitude: &wrapperspb.DoubleValue{Value: 0.1246},
|
|
Timestamp: timestamppb.New(time.Now()),
|
|
Id: uuid.New().String(),
|
|
},
|
|
{
|
|
Latitude: &wrapperspb.DoubleValue{Value: 51.003},
|
|
Longitude: &wrapperspb.DoubleValue{Value: -0.1246},
|
|
Id: uuid.New().String(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
h := testHandler(t)
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.Name, func(t *testing.T) {
|
|
err := h.Save(context.Background(), &pb.SaveRequest{
|
|
Places: tc.Places,
|
|
}, &pb.SaveResponse{})
|
|
assert.Equal(t, tc.Error, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLast(t *testing.T) {
|
|
h := testHandler(t)
|
|
|
|
t.Run("MissingIDs", func(t *testing.T) {
|
|
err := h.Last(context.Background(), &pb.LastRequest{}, &pb.ListResponse{})
|
|
assert.Equal(t, handler.ErrMissingIDs, err)
|
|
})
|
|
|
|
t.Run("NoMatches", func(t *testing.T) {
|
|
var rsp pb.ListResponse
|
|
err := h.Last(context.Background(), &pb.LastRequest{
|
|
Ids: []string{uuid.New().String()},
|
|
}, &rsp)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, rsp.Places)
|
|
})
|
|
tn := time.Now()
|
|
|
|
// generate some example data to work with
|
|
loc1 := &pb.Location{
|
|
Latitude: &wrapperspb.DoubleValue{Value: 51.5007},
|
|
Longitude: &wrapperspb.DoubleValue{Value: 0.1246},
|
|
Timestamp: timestamppb.New(tn),
|
|
Id: "a",
|
|
}
|
|
loc2 := &pb.Location{
|
|
Latitude: &wrapperspb.DoubleValue{Value: 51.6007},
|
|
Longitude: &wrapperspb.DoubleValue{Value: 0.1546},
|
|
Timestamp: timestamppb.New(tn.Add(1 * time.Microsecond)),
|
|
Id: "b",
|
|
}
|
|
loc3 := &pb.Location{
|
|
Latitude: &wrapperspb.DoubleValue{Value: 52.6007},
|
|
Longitude: &wrapperspb.DoubleValue{Value: 0.2546},
|
|
Timestamp: timestamppb.New(tn.Add(2 * time.Microsecond)),
|
|
Id: loc2.Id,
|
|
}
|
|
err := h.Save(context.TODO(), &pb.SaveRequest{
|
|
Places: []*pb.Location{loc1, loc2, loc3},
|
|
}, &pb.SaveResponse{})
|
|
assert.NoError(t, err)
|
|
|
|
t.Run("OneUser", func(t *testing.T) {
|
|
var rsp pb.ListResponse
|
|
err := h.Last(context.Background(), &pb.LastRequest{
|
|
Ids: []string{loc3.Id},
|
|
}, &rsp)
|
|
assert.NoError(t, err)
|
|
|
|
if len(rsp.Places) != 1 {
|
|
t.Fatalf("One location should be returned")
|
|
}
|
|
assert.Equal(t, loc3.Id, rsp.Places[0].Id)
|
|
assert.Equal(t, loc3.Latitude.Value, rsp.Places[0].Latitude.Value)
|
|
assert.Equal(t, loc3.Longitude.Value, rsp.Places[0].Longitude.Value)
|
|
assert.Equal(t, microSecondTime(loc3.Timestamp), microSecondTime(rsp.Places[0].Timestamp))
|
|
})
|
|
|
|
t.Run("ManyUser", func(t *testing.T) {
|
|
var rsp pb.ListResponse
|
|
err := h.Last(context.Background(), &pb.LastRequest{
|
|
Ids: []string{loc1.Id, loc2.Id},
|
|
}, &rsp)
|
|
assert.NoError(t, err)
|
|
|
|
if len(rsp.Places) != 2 {
|
|
t.Fatalf("Two places should be returned")
|
|
}
|
|
|
|
// sort using user_id so we can hardcode the index
|
|
sort.Slice(rsp.Places, func(i, j int) bool {
|
|
return rsp.Places[i].Id > rsp.Places[j].Id
|
|
})
|
|
|
|
assert.Equal(t, loc1.Id, rsp.Places[1].Id)
|
|
assert.Equal(t, loc1.Latitude.Value, rsp.Places[1].Latitude.Value)
|
|
assert.Equal(t, loc1.Longitude.Value, rsp.Places[1].Longitude.Value)
|
|
assert.Equal(t, microSecondTime(loc1.Timestamp), microSecondTime(rsp.Places[1].Timestamp))
|
|
|
|
assert.Equal(t, loc3.Id, rsp.Places[0].Id)
|
|
assert.Equal(t, loc3.Latitude.Value, rsp.Places[0].Latitude.Value)
|
|
assert.Equal(t, loc3.Longitude.Value, rsp.Places[0].Longitude.Value)
|
|
assert.Equal(t, microSecondTime(loc3.Timestamp), microSecondTime(rsp.Places[0].Timestamp))
|
|
})
|
|
}
|
|
|
|
func TestNear(t *testing.T) {
|
|
lat := &wrapperspb.DoubleValue{Value: 51.510357}
|
|
lng := &wrapperspb.DoubleValue{Value: -0.116773}
|
|
rad := &wrapperspb.DoubleValue{Value: 2.0}
|
|
|
|
inBoundsLat := &wrapperspb.DoubleValue{Value: 51.5110}
|
|
inBoundsLng := &wrapperspb.DoubleValue{Value: -0.1142}
|
|
|
|
outOfBoundsLat := &wrapperspb.DoubleValue{Value: 51.5415}
|
|
outOfBoundsLng := &wrapperspb.DoubleValue{Value: -0.0028}
|
|
|
|
tt := []struct {
|
|
Name string
|
|
Places []*pb.Location
|
|
Results []*pb.Location
|
|
QueryLatitude *wrapperspb.DoubleValue
|
|
QueryLongitude *wrapperspb.DoubleValue
|
|
QueryRadius *wrapperspb.DoubleValue
|
|
Error error
|
|
}{
|
|
{
|
|
Name: "MissingLatitude",
|
|
QueryLongitude: lng,
|
|
QueryRadius: rad,
|
|
Error: handler.ErrMissingLatitude,
|
|
},
|
|
{
|
|
Name: "MissingLongitude",
|
|
QueryLatitude: lat,
|
|
QueryRadius: rad,
|
|
Error: handler.ErrMissingLongitude,
|
|
},
|
|
{
|
|
Name: "MissingRadius",
|
|
QueryLatitude: lat,
|
|
QueryLongitude: lng,
|
|
Error: handler.ErrMissingRadius,
|
|
},
|
|
{
|
|
Name: "NoPlaces",
|
|
QueryLatitude: lat,
|
|
QueryLongitude: lng,
|
|
QueryRadius: rad,
|
|
},
|
|
{
|
|
Name: "OneWithinRadius",
|
|
QueryLatitude: lat,
|
|
QueryLongitude: lng,
|
|
QueryRadius: rad,
|
|
Places: []*pb.Location{
|
|
&pb.Location{
|
|
Latitude: inBoundsLat,
|
|
Longitude: inBoundsLng,
|
|
Id: "in",
|
|
},
|
|
&pb.Location{
|
|
Latitude: outOfBoundsLat,
|
|
Longitude: outOfBoundsLng,
|
|
Id: "out",
|
|
},
|
|
},
|
|
Results: []*pb.Location{
|
|
&pb.Location{
|
|
Latitude: inBoundsLat,
|
|
Longitude: inBoundsLng,
|
|
Id: "in",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "NoneWithinRadius",
|
|
QueryLatitude: lat,
|
|
QueryLongitude: lng,
|
|
QueryRadius: &wrapperspb.DoubleValue{Value: 0.01},
|
|
Places: []*pb.Location{
|
|
&pb.Location{
|
|
Latitude: inBoundsLat,
|
|
Longitude: inBoundsLng,
|
|
Id: "in",
|
|
},
|
|
&pb.Location{
|
|
Latitude: outOfBoundsLat,
|
|
Longitude: outOfBoundsLng,
|
|
Id: "out",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "TwoPlacesForUser",
|
|
QueryLatitude: lat,
|
|
QueryLongitude: lng,
|
|
QueryRadius: rad,
|
|
Places: []*pb.Location{
|
|
&pb.Location{
|
|
Latitude: inBoundsLat,
|
|
Longitude: inBoundsLng,
|
|
Id: "in",
|
|
},
|
|
&pb.Location{
|
|
Latitude: outOfBoundsLat,
|
|
Longitude: outOfBoundsLng,
|
|
Id: "out",
|
|
},
|
|
&pb.Location{
|
|
Latitude: inBoundsLat,
|
|
Longitude: inBoundsLng,
|
|
Id: "out",
|
|
},
|
|
},
|
|
Results: []*pb.Location{
|
|
&pb.Location{
|
|
Latitude: inBoundsLat,
|
|
Longitude: inBoundsLng,
|
|
Id: "in",
|
|
},
|
|
&pb.Location{
|
|
Latitude: inBoundsLat,
|
|
Longitude: inBoundsLng,
|
|
Id: "out",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "ManyWithinRadius",
|
|
QueryLatitude: lat,
|
|
QueryLongitude: lng,
|
|
QueryRadius: &wrapperspb.DoubleValue{Value: 20},
|
|
Places: []*pb.Location{
|
|
&pb.Location{
|
|
Latitude: inBoundsLat,
|
|
Longitude: inBoundsLng,
|
|
Id: "in",
|
|
},
|
|
&pb.Location{
|
|
Latitude: outOfBoundsLat,
|
|
Longitude: outOfBoundsLng,
|
|
Id: "out",
|
|
},
|
|
},
|
|
Results: []*pb.Location{
|
|
&pb.Location{
|
|
Latitude: inBoundsLat,
|
|
Longitude: inBoundsLng,
|
|
Id: "in",
|
|
},
|
|
&pb.Location{
|
|
Latitude: outOfBoundsLat,
|
|
Longitude: outOfBoundsLng,
|
|
Id: "out",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.Name, func(t *testing.T) {
|
|
h := testHandler(t)
|
|
|
|
// create the places
|
|
if len(tc.Places) > 0 {
|
|
err := h.Save(context.TODO(), &pb.SaveRequest{Places: tc.Places}, &pb.SaveResponse{})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
// find near places
|
|
var rsp pb.ListResponse
|
|
err := h.Near(context.TODO(), &pb.NearRequest{
|
|
Latitude: tc.QueryLatitude,
|
|
Longitude: tc.QueryLongitude,
|
|
Radius: tc.QueryRadius,
|
|
}, &rsp)
|
|
assert.Equal(t, tc.Error, err)
|
|
|
|
// check the count of the results matches
|
|
if len(tc.Results) != len(rsp.Places) {
|
|
t.Errorf("Incorrect number of results returned. Expected %v, got %v", len(tc.Results), len(rsp.Places))
|
|
}
|
|
|
|
// validate the results match
|
|
sort.Slice(rsp.Places, func(i, j int) bool {
|
|
return rsp.Places[i].Id > rsp.Places[j].Id
|
|
})
|
|
sort.Slice(tc.Results, func(i, j int) bool {
|
|
return tc.Results[i].Id > tc.Results[j].Id
|
|
})
|
|
for i, r := range tc.Results {
|
|
l := rsp.Places[i]
|
|
assert.Equal(t, r.Id, l.Id)
|
|
assert.Equal(t, r.Latitude.Value, l.Latitude.Value)
|
|
assert.Equal(t, r.Longitude.Value, l.Longitude.Value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRead(t *testing.T) {
|
|
h := testHandler(t)
|
|
|
|
baseTime := time.Now().Add(time.Hour * -24)
|
|
|
|
t.Run("MissingIDs", func(t *testing.T) {
|
|
err := h.Read(context.Background(), &pb.ReadRequest{
|
|
After: timestamppb.New(baseTime),
|
|
Before: timestamppb.New(baseTime),
|
|
}, &pb.ListResponse{})
|
|
assert.Equal(t, handler.ErrMissingIDs, err)
|
|
})
|
|
|
|
t.Run("MissingAfter", func(t *testing.T) {
|
|
err := h.Read(context.Background(), &pb.ReadRequest{
|
|
Ids: []string{uuid.New().String()},
|
|
Before: timestamppb.New(baseTime),
|
|
}, &pb.ListResponse{})
|
|
assert.Equal(t, handler.ErrMissingAfter, err)
|
|
})
|
|
|
|
t.Run("MissingBefore", func(t *testing.T) {
|
|
err := h.Read(context.Background(), &pb.ReadRequest{
|
|
Ids: []string{uuid.New().String()},
|
|
After: timestamppb.New(baseTime),
|
|
}, &pb.ListResponse{})
|
|
assert.Equal(t, handler.ErrMissingBefore, err)
|
|
})
|
|
|
|
// generate some example data to work with
|
|
loc1 := &pb.Location{
|
|
Latitude: &wrapperspb.DoubleValue{Value: 51.5007},
|
|
Longitude: &wrapperspb.DoubleValue{Value: 0.1246},
|
|
Timestamp: timestamppb.New(baseTime.Add(time.Minute * 10)),
|
|
Id: "a",
|
|
}
|
|
loc2 := &pb.Location{
|
|
Latitude: &wrapperspb.DoubleValue{Value: 51.6007},
|
|
Longitude: &wrapperspb.DoubleValue{Value: 0.1546},
|
|
Timestamp: timestamppb.New(baseTime.Add(time.Minute * 20)),
|
|
Id: "b",
|
|
}
|
|
loc3 := &pb.Location{
|
|
Latitude: &wrapperspb.DoubleValue{Value: 52.6007},
|
|
Longitude: &wrapperspb.DoubleValue{Value: 0.2546},
|
|
Timestamp: timestamppb.New(baseTime.Add(time.Minute * 40)),
|
|
Id: loc2.Id,
|
|
}
|
|
err := h.Save(context.TODO(), &pb.SaveRequest{
|
|
Places: []*pb.Location{loc1, loc2, loc3},
|
|
}, &pb.SaveResponse{})
|
|
assert.NoError(t, err)
|
|
|
|
t.Run("NoMatches", func(t *testing.T) {
|
|
var rsp pb.ListResponse
|
|
err := h.Read(context.Background(), &pb.ReadRequest{
|
|
Ids: []string{uuid.New().String()},
|
|
After: timestamppb.New(baseTime),
|
|
Before: timestamppb.New(baseTime.Add(time.Hour)),
|
|
}, &rsp)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, rsp.Places)
|
|
})
|
|
|
|
t.Run("OnePlaceID", func(t *testing.T) {
|
|
var rsp pb.ListResponse
|
|
err := h.Read(context.Background(), &pb.ReadRequest{
|
|
Ids: []string{loc2.Id},
|
|
After: timestamppb.New(baseTime),
|
|
Before: timestamppb.New(baseTime.Add(time.Hour)),
|
|
}, &rsp)
|
|
assert.NoError(t, err)
|
|
|
|
if len(rsp.Places) != 2 {
|
|
t.Fatalf("Two places should be returned")
|
|
}
|
|
assert.Equal(t, loc2.Id, rsp.Places[0].Id)
|
|
assert.Equal(t, loc2.Latitude.Value, rsp.Places[0].Latitude.Value)
|
|
assert.Equal(t, loc2.Longitude.Value, rsp.Places[0].Longitude.Value)
|
|
assert.Equal(t, microSecondTime(loc2.Timestamp), microSecondTime(rsp.Places[0].Timestamp))
|
|
|
|
assert.Equal(t, loc3.Id, rsp.Places[1].Id)
|
|
assert.Equal(t, loc3.Latitude.Value, rsp.Places[1].Latitude.Value)
|
|
assert.Equal(t, loc3.Longitude.Value, rsp.Places[1].Longitude.Value)
|
|
assert.Equal(t, microSecondTime(loc3.Timestamp), microSecondTime(rsp.Places[1].Timestamp))
|
|
})
|
|
|
|
t.Run("OnePlaceIDReducedTime", func(t *testing.T) {
|
|
var rsp pb.ListResponse
|
|
err := h.Read(context.Background(), &pb.ReadRequest{
|
|
Ids: []string{loc2.Id},
|
|
After: timestamppb.New(baseTime),
|
|
Before: timestamppb.New(baseTime.Add(time.Minute * 30)),
|
|
}, &rsp)
|
|
assert.NoError(t, err)
|
|
|
|
if len(rsp.Places) != 1 {
|
|
t.Fatalf("One location should be returned")
|
|
}
|
|
assert.Equal(t, loc2.Id, rsp.Places[0].Id)
|
|
assert.Equal(t, loc2.Latitude.Value, rsp.Places[0].Latitude.Value)
|
|
assert.Equal(t, loc2.Longitude.Value, rsp.Places[0].Longitude.Value)
|
|
assert.Equal(t, microSecondTime(loc2.Timestamp), microSecondTime(rsp.Places[0].Timestamp))
|
|
})
|
|
|
|
t.Run("TwoPlaceIDs", func(t *testing.T) {
|
|
var rsp pb.ListResponse
|
|
err := h.Read(context.Background(), &pb.ReadRequest{
|
|
Ids: []string{loc1.Id, loc2.Id},
|
|
After: timestamppb.New(baseTime),
|
|
Before: timestamppb.New(baseTime.Add(time.Minute * 30)),
|
|
}, &rsp)
|
|
assert.NoError(t, err)
|
|
|
|
if len(rsp.Places) != 2 {
|
|
t.Fatalf("Two places should be returned")
|
|
}
|
|
assert.Equal(t, loc1.Id, rsp.Places[0].Id)
|
|
assert.Equal(t, loc1.Latitude.Value, rsp.Places[0].Latitude.Value)
|
|
assert.Equal(t, loc1.Longitude.Value, rsp.Places[0].Longitude.Value)
|
|
assert.Equal(t, microSecondTime(loc1.Timestamp), microSecondTime(rsp.Places[0].Timestamp))
|
|
|
|
assert.Equal(t, loc2.Id, rsp.Places[1].Id)
|
|
assert.Equal(t, loc2.Latitude.Value, rsp.Places[1].Latitude.Value)
|
|
assert.Equal(t, loc2.Longitude.Value, rsp.Places[1].Longitude.Value)
|
|
assert.Equal(t, microSecondTime(loc2.Timestamp), microSecondTime(rsp.Places[1].Timestamp))
|
|
})
|
|
}
|
|
|
|
// postgres has a resolution of 100microseconds so just test that it's accurate to the second
|
|
func microSecondTime(t *timestamp.Timestamp) time.Time {
|
|
tt := t.AsTime()
|
|
return time.Unix(tt.Unix(), int64(tt.Nanosecond()-tt.Nanosecond()%1000))
|
|
}
|