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