package routing import ( "encoding/json" "net/http" "net/http/httptest" "testing" ) // stubOptimizer stands in for the Route Optimization API. It captures the // request body so the outbound contract can be asserted, and returns whatever // the test tells it to. func stubOptimizer(t *testing.T, captured *optimizeRequest, respond func(optimizeRequest) optimizeResponse) *httptest.Server { t.Helper() return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != optimizePath { t.Errorf("posted to %s, want %s", r.URL.Path, optimizePath) } var req optimizeRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Errorf("stub could not decode request: %v", err) } if captured != nil { *captured = req } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(respond(req)) })) } func testStops() []stop { return []stop{ {AssignmentID: 1, BookingID: 101, BookingNo: "A", PickupLat: 11.0045, PickupLng: 76.9612, DeliveryLat: 11.0510, DeliveryLng: 76.9300}, {AssignmentID: 2, BookingID: 102, BookingNo: "B", PickupLat: 11.0045, PickupLng: 76.9612, DeliveryLat: 11.0168, DeliveryLng: 76.9558}, {AssignmentID: 3, BookingID: 103, BookingNo: "C", PickupLat: 11.0045, PickupLng: 76.9612, DeliveryLat: 10.9938, DeliveryLng: 76.9954}, } } // The whole point of the Doormile endpoint is that it takes Doormile's field // names. Sending the provider ones (pickuplat/deliverylat) against the provider // endpoint returns HTTP 200 with everything silently zeroed, so this contract is // worth pinning rather than trusting. func TestOptimizeSendsDoormileFieldNames(t *testing.T) { var got optimizeRequest srv := stubOptimizer(t, &got, func(req optimizeRequest) optimizeResponse { return optimizeResponse{Success: true, Stops: []optimizeResponseStop{ {Bookingassignmentid: 1, Bookingid: 101, Step: 1}, }} }) defer srv.Close() BaseURL = srv.URL defer func() { BaseURL = "" }() if _, err := optimize(testStops()); err != nil { t.Fatalf("optimize: %v", err) } if len(got.Bookings) != 3 { t.Fatalf("sent %d bookings, want 3", len(got.Bookings)) } first := got.Bookings[0] if first.Bookingid != 101 || first.Bookingassignmentid != 1 { t.Errorf("identity fields wrong: %+v", first) } // Coordinates must survive as real numbers, not be rounded or stringified // into a different place. if first.Pickuplatitude != 11.0045 || first.Deliverylongitude != 76.93 { t.Errorf("coordinates mangled: %+v", first) } } func TestOptimizeMapsResultsBackToAssignments(t *testing.T) { srv := stubOptimizer(t, nil, func(req optimizeRequest) optimizeResponse { // Reverse the order, as a real resequencing would. return optimizeResponse{Success: true, Stops: []optimizeResponseStop{ {Bookingassignmentid: 3, Bookingid: 103, Step: 1, Previouskms: 4, Cumulativekms: 4, Etaminutes: 14, Cumulativeeta: 14}, {Bookingassignmentid: 2, Bookingid: 102, Step: 2, Previouskms: 5, Cumulativekms: 9, Etaminutes: 8, Cumulativeeta: 22}, {Bookingassignmentid: 1, Bookingid: 101, Step: 3, Previouskms: 5, Cumulativekms: 14, Etaminutes: 8, Cumulativeeta: 30}, }} }) defer srv.Close() BaseURL = srv.URL defer func() { BaseURL = "" }() results, err := optimize(testStops()) if err != nil { t.Fatalf("optimize: %v", err) } if len(results) != 3 { t.Fatalf("got %d results, want 3", len(results)) } if results[0].AssignmentID != 3 || results[0].Step != 1 || results[0].CumulativeETA != 14 { t.Errorf("first result wrong: %+v", results[0]) } if results[2].AssignmentID != 1 || results[2].CumulativeKM != 14 { t.Errorf("last result wrong: %+v", results[2]) } } // These steps get written straight onto assignment rows, so a step for an // assignment we never sent must never make it through — it would reorder some // other rider's work. func TestOptimizeDiscardsUnknownAssignments(t *testing.T) { srv := stubOptimizer(t, nil, func(req optimizeRequest) optimizeResponse { return optimizeResponse{Success: true, Stops: []optimizeResponseStop{ {Bookingassignmentid: 999, Bookingid: 999, Step: 1}, {Bookingassignmentid: 2, Bookingid: 102, Step: 2, Cumulativekms: 9}, }} }) defer srv.Close() BaseURL = srv.URL defer func() { BaseURL = "" }() results, err := optimize(testStops()) if err != nil { t.Fatalf("optimize: %v", err) } if len(results) != 1 || results[0].AssignmentID != 2 { t.Fatalf("unknown assignment leaked through: %+v", results) } } // Step 0 means "not sequenced". Persisting it would read as a position. func TestOptimizeDropsZeroSteps(t *testing.T) { srv := stubOptimizer(t, nil, func(req optimizeRequest) optimizeResponse { return optimizeResponse{Success: true, Stops: []optimizeResponseStop{ {Bookingassignmentid: 1, Bookingid: 101, Step: 0}, {Bookingassignmentid: 2, Bookingid: 102, Step: 1}, }} }) defer srv.Close() BaseURL = srv.URL defer func() { BaseURL = "" }() results, err := optimize(testStops()) if err != nil { t.Fatalf("optimize: %v", err) } if len(results) != 1 || results[0].AssignmentID != 2 { t.Fatalf("step 0 was kept: %+v", results) } } func TestOptimizeErrorsAreSurfacedNotSilent(t *testing.T) { cases := []struct { name string handler http.HandlerFunc }{ {"http 500", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) }}, {"success false", func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(optimizeResponse{Success: false}) }}, {"empty stops", func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(optimizeResponse{Success: true, Stops: nil}) }}, {"garbage body", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("not json")) }}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { srv := httptest.NewServer(tc.handler) defer srv.Close() BaseURL = srv.URL defer func() { BaseURL = "" }() if _, err := optimize(testStops()); err == nil { t.Fatal("expected an error, got nil — a failed optimise must not look like success") } }) } }