package tests import ( "bytes" "encoding/json" "fmt" "math" "net/http" "net/http/httptest" "testing" "gestion/handlers" "github.com/gin-gonic/gin" ) // UpdateDeliveryStatus (handlers/deleviry.go) n'impose plus aucune limite de // distance entre le livreur et la destination pour valider une livraison // (statut "livre") — la vérification GPS a été volontairement retirée pour ne // pas bloquer le livreur (l'imprécision GPS réelle en zone urbaine/immeuble // provoquait des rejets sur des livraisons pourtant légitimes). Les // coordonnées GPS restent obligatoires et la distance est toujours calculée // et loguée à des fins de suivi, mais elle n'entraîne plus de rejet. const earthRadiusMeters = 6371000.0 // destinationPointNorthOf renvoie un point situé à "meters" au nord de // (lat, lon) — même longitude, donc distance ≈ purement le delta de latitude // (formule identique à utils.CalculateDistance pour ce cas particulier). func destinationPointNorthOf(lat, lon, meters float64) (float64, float64) { latOffsetRad := meters / earthRadiusMeters latOffsetDeg := latOffsetRad * (180 / math.Pi) return lat + latOffsetDeg, lon } func setCommandDestination(t *testing.T, commandID int, lat, lon float64) { t.Helper() if err := testDB.GDB.Exec( `UPDATE commandes SET dest_latitude = ?, dest_longitude = ? WHERE id = ?`, lat, lon, commandID, ).Error; err != nil { t.Fatalf("setCommandDestination: %v", err) } } // deliveryStatusContextJSON est la variante de deliveryStatusContext (voir // penalty_test.go) qui accepte un corps JSON arbitraire — nécessaire ici pour // pouvoir passer latitude/longitude, que l'helper existant ne supporte pas. func deliveryStatusContextJSON(username string, commandID int, body []byte) (*gin.Context, *httptest.ResponseRecorder) { req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/livreur/deliveries/%d/status", commandID), bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) c.Request = req c.Params = gin.Params{{Key: "id", Value: fmt.Sprintf("%d", commandID)}} c.Set("database", testDB) c.Set("username", username) c.Set("role", "livreur") return c, rec } const nantesLat, nantesLon = 47.2184, -1.5536 func TestUpdateDeliveryStatus_GPS_ValidatesDeliveryAtModerateDistance(t *testing.T) { cleanupStockTestData(t) livreur := newTestClient(t, "gps_livreur_within") client := newTestClient(t, "gps_client_within") productID := newTestProduct(t, "GPSWithin", 10) cmdID := newTestCommandWithItem(t, client, "en_route", livreur, productID, 1, 10) setCommandDestination(t, cmdID, nantesLat, nantesLon) livreurLat, livreurLon := destinationPointNorthOf(nantesLat, nantesLon, 200) body, _ := json.Marshal(map[string]any{"status": "livre", "latitude": livreurLat, "longitude": livreurLon}) c, rec := deliveryStatusContextJSON(livreur, cmdID, body) handlers.UpdateDeliveryStatus(c) if rec.Code != http.StatusOK { t.Fatalf("status HTTP: got=%d body=%s", rec.Code, rec.Body.String()) } if got := commandStatus(t, cmdID); got != "livre" { t.Errorf("statut après validation à 200m: got=%s want=livre", got) } } // Aucune distance, aussi grande soit-elle, ne doit bloquer la validation : la // vérification GPS a été retirée pour ne jamais empêcher un livreur de // marquer une commande "livre". func TestUpdateDeliveryStatus_GPS_FarBeyondOldThresholdStillValidatesDelivery(t *testing.T) { cleanupStockTestData(t) livreur := newTestClient(t, "gps_livreur_far") client := newTestClient(t, "gps_client_far") productID := newTestProduct(t, "GPSFar", 10) cmdID := newTestCommandWithItem(t, client, "en_route", livreur, productID, 1, 10) setCommandDestination(t, cmdID, nantesLat, nantesLon) livreurLat, livreurLon := destinationPointNorthOf(nantesLat, nantesLon, 5000) // 5km : loin de toute ancienne limite body, _ := json.Marshal(map[string]any{"status": "livre", "latitude": livreurLat, "longitude": livreurLon}) c, rec := deliveryStatusContextJSON(livreur, cmdID, body) handlers.UpdateDeliveryStatus(c) if rec.Code != http.StatusOK { t.Fatalf("aucune distance ne doit bloquer la validation: got=%d body=%s", rec.Code, rec.Body.String()) } if got := commandStatus(t, cmdID); got != "livre" { t.Errorf("statut après validation à 5km: got=%s want=livre", got) } } func TestUpdateDeliveryStatus_GPS_MissingCoordinatesRejected(t *testing.T) { cleanupStockTestData(t) livreur := newTestClient(t, "gps_livreur_missing_coords") client := newTestClient(t, "gps_client_missing_coords") productID := newTestProduct(t, "GPSMissingCoords", 10) cmdID := newTestCommandWithItem(t, client, "en_route", livreur, productID, 1, 10) setCommandDestination(t, cmdID, nantesLat, nantesLon) body, _ := json.Marshal(map[string]any{"status": "livre"}) // latitude/longitude absents (zéro) c, rec := deliveryStatusContextJSON(livreur, cmdID, body) handlers.UpdateDeliveryStatus(c) if rec.Code != http.StatusBadRequest { t.Fatalf("status HTTP sans coordonnées: got=%d want=%d body=%s", rec.Code, http.StatusBadRequest, rec.Body.String()) } if got := commandStatus(t, cmdID); got != "en_route" { t.Errorf("le statut ne doit pas changer sans coordonnées GPS: got=%s want=en_route", got) } } // Si la commande n'a pas de coordonnées de destination enregistrées (adresse // non géocodée), la validation GPS est ignorée plutôt que de bloquer le // livreur indéfiniment. func TestUpdateDeliveryStatus_GPS_MissingDestinationCoordinatesSkipsValidation(t *testing.T) { cleanupStockTestData(t) livreur := newTestClient(t, "gps_livreur_no_dest") client := newTestClient(t, "gps_client_no_dest") productID := newTestProduct(t, "GPSNoDest", 10) cmdID := newTestCommandWithItem(t, client, "en_route", livreur, productID, 1, 10) // Pas d'appel à setCommandDestination : dest_latitude/dest_longitude restent à 0/NULL. body, _ := json.Marshal(map[string]any{"status": "livre", "latitude": 48.8566, "longitude": 2.3522}) // Paris, sans rapport c, rec := deliveryStatusContextJSON(livreur, cmdID, body) handlers.UpdateDeliveryStatus(c) if rec.Code != http.StatusOK { t.Fatalf("sans destination enregistrée, la validation GPS doit être ignorée: got=%d body=%s", rec.Code, rec.Body.String()) } if got := commandStatus(t, cmdID); got != "livre" { t.Errorf("statut: got=%s want=livre", got) } } func TestUpdateDeliveryStatus_RejectsWhenNotAssignedToThisLivreur(t *testing.T) { cleanupStockTestData(t) assignedLivreur := newTestClient(t, "gps_assigned_livreur") intruder := newTestClient(t, "gps_intruder_livreur") client := newTestClient(t, "gps_client_wrong_livreur") productID := newTestProduct(t, "GPSWrongLivreur", 10) cmdID := newTestCommandWithItem(t, client, "en_route", assignedLivreur, productID, 1, 10) setCommandDestination(t, cmdID, nantesLat, nantesLon) body, _ := json.Marshal(map[string]any{"status": "livre", "latitude": nantesLat, "longitude": nantesLon}) c, rec := deliveryStatusContextJSON(intruder, cmdID, body) handlers.UpdateDeliveryStatus(c) if rec.Code != http.StatusForbidden { t.Fatalf("un livreur non assigné doit être rejeté: got=%d want=%d body=%s", rec.Code, http.StatusForbidden, rec.Body.String()) } if got := commandStatus(t, cmdID); got != "en_route" { t.Errorf("le statut ne doit pas changer: got=%s want=en_route", got) } }