solidarity-transport/handler/journeys_test.go

335 lines
10 KiB
Go

package handler
import (
"testing"
"time"
"git.coopgo.io/coopgo-platform/routing-service"
"git.coopgo.io/coopgo-platform/solidarity-transport/storage"
"git.coopgo.io/coopgo-platform/solidarity-transport/types"
"github.com/google/uuid"
"github.com/paulmach/orb"
"github.com/paulmach/orb/geojson"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type MockRoutingService struct {
mock.Mock
}
func (m *MockRoutingService) Route(points []orb.Point) (*routing.Route, error) {
args := m.Called(points)
return args.Get(0).(*routing.Route), args.Error(1)
}
func createTestConfig() *viper.Viper {
cfg := viper.New()
cfg.Set("storage.db.type", "mock")
cfg.Set("parameters.limits.distance.min", 1000)
cfg.Set("parameters.limits.distance.max", 50000)
return cfg
}
func createTestGeoJSONFeature(lat, lng float64, label string) *geojson.Feature {
feature := geojson.NewFeature(orb.Point{lng, lat})
feature.Properties = make(map[string]interface{})
feature.Properties["label"] = label
return feature
}
func createTestRoute() *routing.Route {
return createTestRouteWithDistance(6000) // Default route with 6km passenger distance
}
func createTestRouteNoReturn(passengerDistance float64) *routing.Route {
// No-return: 3 legs (driver→pickup, pickup→dropoff, dropoff→driver)
return &routing.Route{
Summary: routing.Summary{
Distance: 4000 + passengerDistance,
Duration: 15 * time.Minute,
Polyline: "test_polyline",
},
Legs: []routing.RouteLeg{
{Summary: routing.Summary{Distance: 2000, Duration: 5 * time.Minute}}, // driver to pickup
{Summary: routing.Summary{Distance: passengerDistance, Duration: 8 * time.Minute}}, // pickup to dropoff
{Summary: routing.Summary{Distance: 2000, Duration: 5 * time.Minute}}, // dropoff to driver destination
},
}
}
func createTestRouteReturn(passengerDistance float64) *routing.Route {
// Return: 4+ legs (driver→pickup, pickup→dropoff, dropoff→pickup, pickup→driver)
// For return journeys: passenger distance = legs[1] + legs[2]
singleLegDistance := passengerDistance / 2
return &routing.Route{
Summary: routing.Summary{
Distance: 4000 + passengerDistance,
Duration: 18 * time.Minute,
Polyline: "test_polyline",
},
Legs: []routing.RouteLeg{
{Summary: routing.Summary{Distance: 2000, Duration: 5 * time.Minute}}, // driver to pickup
{Summary: routing.Summary{Distance: singleLegDistance, Duration: 8 * time.Minute}}, // pickup to dropoff
{Summary: routing.Summary{Distance: singleLegDistance, Duration: 2 * time.Minute}}, // dropoff to pickup (return)
{Summary: routing.Summary{Distance: 2000, Duration: 3 * time.Minute}}, // pickup to driver destination
},
}
}
func createTestRouteWithDistance(passengerDistance float64) *routing.Route {
// Default to return journey for backward compatibility
return createTestRouteReturn(passengerDistance)
}
func createTestAvailability(driverID string, day int, startTime, endTime string) *types.DriverRegularAvailability {
address := createTestGeoJSONFeature(45.7640, 4.8357, "Driver Address")
return &types.DriverRegularAvailability{
ID: uuid.New().String(),
DriverId: driverID,
Day: day,
StartTime: startTime,
EndTime: endTime,
Address: address,
}
}
func TestHandler_GetDriverJourneys(t *testing.T) {
tests := []struct {
name string
departureDate time.Time
noreturn bool
availabilities []*types.DriverRegularAvailability
routingResponse *routing.Route
expectedJourneys int
expectedDriverIDs []string
description string
}{
{
name: "journey within distance limits",
departureDate: time.Date(2024, 1, 15, 8, 0, 0, 0, time.UTC),
noreturn: false,
availabilities: []*types.DriverRegularAvailability{
createTestAvailability("driver-1", 1, "07:00", "09:00"),
},
routingResponse: createTestRouteWithDistance(6000), // 6km - within limits (1km to 50km)
expectedJourneys: 1,
expectedDriverIDs: []string{"driver-1"},
description: "6km journey should be within distance limits",
},
{
name: "journey too short - below minimum distance",
departureDate: time.Date(2024, 1, 15, 8, 0, 0, 0, time.UTC),
noreturn: false,
availabilities: []*types.DriverRegularAvailability{
createTestAvailability("driver-1", 1, "07:00", "09:00"),
},
routingResponse: createTestRouteWithDistance(500), // 500m - below 1km minimum
expectedJourneys: 0,
expectedDriverIDs: []string{},
description: "500m journey should be rejected (below minimum distance)",
},
{
name: "journey too long - above maximum distance",
departureDate: time.Date(2024, 1, 15, 8, 0, 0, 0, time.UTC),
noreturn: false,
availabilities: []*types.DriverRegularAvailability{
createTestAvailability("driver-1", 1, "07:00", "09:00"),
},
routingResponse: createTestRouteWithDistance(60000), // 60km - above 50km maximum
expectedJourneys: 0,
expectedDriverIDs: []string{},
description: "60km journey should be rejected (above maximum distance)",
},
{
name: "journey at minimum distance boundary",
departureDate: time.Date(2024, 1, 15, 8, 0, 0, 0, time.UTC),
noreturn: false,
availabilities: []*types.DriverRegularAvailability{
createTestAvailability("driver-1", 1, "07:00", "09:00"),
},
routingResponse: createTestRouteWithDistance(1000), // Exactly 1km minimum
expectedJourneys: 1,
expectedDriverIDs: []string{"driver-1"},
description: "1km journey should be accepted (at minimum boundary)",
},
{
name: "journey at maximum distance boundary",
departureDate: time.Date(2024, 1, 15, 8, 0, 0, 0, time.UTC),
noreturn: false,
availabilities: []*types.DriverRegularAvailability{
createTestAvailability("driver-1", 1, "07:00", "09:00"),
},
routingResponse: createTestRouteWithDistance(50000), // Exactly 50km maximum
expectedJourneys: 1,
expectedDriverIDs: []string{"driver-1"},
description: "50km journey should be accepted (at maximum boundary)",
},
{
name: "wrong day no availabilities",
departureDate: time.Date(2024, 1, 16, 9, 0, 0, 0, time.UTC),
noreturn: false,
availabilities: []*types.DriverRegularAvailability{
createTestAvailability("driver-1", 1, "08:00", "10:00"),
},
expectedJourneys: 0,
expectedDriverIDs: []string{},
description: "Tuesday departure should not match Monday availability",
},
}
departure := createTestGeoJSONFeature(45.7578, 4.8320, "Departure")
arrival := createTestGeoJSONFeature(45.7485, 4.8467, "Arrival")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := createTestConfig()
mockStorage := storage.NewMockStorage(cfg)
mockRouting := &MockRoutingService{}
for _, availability := range tt.availabilities {
err := mockStorage.CreateDriverRegularAvailability(*availability)
require.NoError(t, err)
}
routeResponse := tt.routingResponse
if routeResponse == nil {
routeResponse = createTestRoute() // Default route
}
mockRouting.On("Route", mock.AnythingOfType("[]orb.Point")).Return(routeResponse, nil)
handler := &Handler{
Config: cfg,
Storage: mockStorage,
Routing: mockRouting,
}
journeys, err := handler.GetDriverJourneys(departure, arrival, tt.departureDate, tt.noreturn)
assert.NoError(t, err, tt.description)
assert.Len(t, journeys, tt.expectedJourneys, tt.description)
if tt.expectedJourneys > 0 {
actualDriverIDs := make([]string, len(journeys))
for i, journey := range journeys {
assert.NotEmpty(t, journey.Id)
actualDriverIDs[i] = journey.DriverId
}
assert.ElementsMatch(t, tt.expectedDriverIDs, actualDriverIDs, tt.description)
}
})
}
}
func TestHandler_GetDriverJourney(t *testing.T) {
cfg := createTestConfig()
mockStorage := storage.NewMockStorage(cfg)
testJourney := &types.DriverJourney{
Id: uuid.New().String(),
DriverId: "driver-1",
}
err := mockStorage.PushDriverJourneys([]*types.DriverJourney{testJourney})
require.NoError(t, err)
handler := &Handler{
Config: cfg,
Storage: mockStorage,
}
tests := []struct {
name string
driverID string
journeyID string
expectedError bool
errorMessage string
}{
{
name: "successful retrieval",
driverID: "driver-1",
journeyID: testJourney.Id,
expectedError: false,
},
{
name: "driver mismatch",
driverID: "driver-2",
journeyID: testJourney.Id,
expectedError: true,
errorMessage: "not allowed, driver id mismatch",
},
{
name: "journey not found",
driverID: "driver-1",
journeyID: "non-existent",
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
journey, err := handler.GetDriverJourney(tt.driverID, tt.journeyID)
if tt.expectedError {
assert.Error(t, err)
if tt.errorMessage != "" {
assert.Contains(t, err.Error(), tt.errorMessage)
}
assert.Nil(t, journey)
} else {
assert.NoError(t, err)
assert.NotNil(t, journey)
assert.Equal(t, tt.journeyID, journey.Id)
assert.Equal(t, tt.driverID, journey.DriverId)
}
})
}
}
func TestHandler_ToggleDriverJourneyNoreturn(t *testing.T) {
cfg := createTestConfig()
mockStorage := storage.NewMockStorage(cfg)
mockRouting := &MockRoutingService{}
departure := createTestGeoJSONFeature(45.7578, 4.8320, "Departure")
arrival := createTestGeoJSONFeature(45.7485, 4.8467, "Arrival")
driverAddress := createTestGeoJSONFeature(45.7640, 4.8357, "Driver Address")
testJourney := &types.DriverJourney{
Id: uuid.New().String(),
DriverId: "driver-1",
PassengerPickup: departure,
PassengerDrop: arrival,
DriverDeparture: driverAddress,
Noreturn: false,
}
err := mockStorage.PushDriverJourneys([]*types.DriverJourney{testJourney})
require.NoError(t, err)
mockRouting.On("Route", mock.AnythingOfType("[]orb.Point")).Return(createTestRoute(), nil)
handler := &Handler{
Config: cfg,
Storage: mockStorage,
Routing: mockRouting,
}
err = handler.ToggleDriverJourneyNoreturn(testJourney.Id)
assert.NoError(t, err)
updatedJourney, err := mockStorage.GetDriverJourney(testJourney.Id)
assert.NoError(t, err)
assert.True(t, updatedJourney.Noreturn)
err = handler.ToggleDriverJourneyNoreturn(testJourney.Id)
assert.NoError(t, err)
updatedJourney, err = mockStorage.GetDriverJourney(testJourney.Id)
assert.NoError(t, err)
assert.False(t, updatedJourney.Noreturn)
mockRouting.AssertExpectations(t)
}