package services import ( "math" "testing" ) // Repères réels de Nantes (44000) utilisés pour vérifier le calcul de // distance/temps de trajet des commandes. var ( placeRoyale = Coordinates{Latitude: 47.2148, Longitude: -1.5584} gareNantes = Coordinates{Latitude: 47.2173, Longitude: -1.5426} aeroportNantes = Coordinates{Latitude: 47.1532, Longitude: -1.6107} ) func almostEqual(a, b, tolerance float64) bool { return math.Abs(a-b) <= tolerance } func TestCalculateDistance_SamePointIsZero(t *testing.T) { if got := CalculateDistance(placeRoyale, placeRoyale); got != 0 { t.Errorf("distance entre un point et lui-même: got=%.4f want=0", got) } } // Le long d'un même méridien (même longitude), la distance Haversine est // exacte : 1° de latitude = R * (π/180) ≈ 111.19 km. func TestCalculateDistance_OneDegreeLatitudeIsExact(t *testing.T) { from := Coordinates{Latitude: 47.0, Longitude: -1.5536} to := Coordinates{Latitude: 48.0, Longitude: -1.5536} want := EarthRadiusKm * (math.Pi / 180.0) got := CalculateDistance(from, to) if !almostEqual(got, want, 0.01) { t.Errorf("distance 1° de latitude: got=%.4f want=%.4f", got, want) } } func TestCalculateDistance_IsSymmetric(t *testing.T) { d1 := CalculateDistance(placeRoyale, gareNantes) d2 := CalculateDistance(gareNantes, placeRoyale) if !almostEqual(d1, d2, 0.0001) { t.Errorf("la distance doit être symétrique: A->B=%.4f B->A=%.4f", d1, d2) } } // Place Royale <-> Aéroport de Nantes : environ 8 km à vol d'oiseau. func TestCalculateDistance_RealNantesLandmarks(t *testing.T) { got := CalculateDistance(placeRoyale, aeroportNantes) if got < 6 || got > 10 { t.Errorf("distance Place Royale -> Aéroport Nantes hors plage réaliste: got=%.2f km, want=[6,10]", got) } } func TestCalculateETA_VeryCloseReturnsMinETA(t *testing.T) { cases := []float64{0, 0.01, 0.05, 0.099} for _, d := range cases { if got := CalculateETA(d); got != MinETA { t.Errorf("CalculateETA(%.3f km): got=%d want=%d (MinETA)", d, got, MinETA) } } } // Formule : (distance/25 km/h)*60 min, +20% de marge trafic, arrondi par troncature. func TestCalculateETA_MatchesFormulaForNormalDistance(t *testing.T) { distanceKm := 10.0 travelTime := (distanceKm / 25.0) * 60.0 want := int(travelTime * 1.2) got := CalculateETA(distanceKm) if got != want { t.Errorf("CalculateETA(%.1f km): got=%d want=%d", distanceKm, got, want) } } func TestCalculateETA_VeryFarClampsToMaxETA(t *testing.T) { if got := CalculateETA(1000); got != MaxETA { t.Errorf("CalculateETA(1000 km): got=%d want=%d (MaxETA)", got, MaxETA) } } // L'ETA ne doit jamais sortir de l'intervalle [MinETA, MaxETA], quelle que // soit la distance fournie (y compris des valeurs aberrantes). func TestCalculateETA_AlwaysWithinBounds(t *testing.T) { distances := []float64{-5, 0, 0.05, 1, 5, 10, 50, 100, 500, 10000} for _, d := range distances { got := CalculateETA(d) if got < MinETA || got > MaxETA { t.Errorf("CalculateETA(%.2f): got=%d, hors bornes [%d,%d]", d, got, MinETA, MaxETA) } } } // Sans clé TomTom configurée (cas de cet environnement de test), le calcul // doit retomber sur Haversine + CalculateETA, sans appel réseau. func TestCalculateETAWithTomTom_FallsBackToHaversineWithoutAPIKey(t *testing.T) { if len(tomTomKeys.keys) != 0 { t.Skip("test valable uniquement sans clé TomTom configurée dans l'environnement") } wantDistance := CalculateDistance(placeRoyale, aeroportNantes) wantETA := CalculateETA(wantDistance) gotETA, gotDistance, err := CalculateETAWithTomTom(placeRoyale, aeroportNantes) if err != nil { t.Fatalf("CalculateETAWithTomTom (fallback): %v", err) } if gotDistance != wantDistance { t.Errorf("distance fallback: got=%.4f want=%.4f", gotDistance, wantDistance) } if gotETA != wantETA { t.Errorf("ETA fallback: got=%d want=%d", gotETA, wantETA) } } func TestValidateCoordinates(t *testing.T) { cases := []struct { name string coords Coordinates wantErr bool }{ {"Nantes valide", placeRoyale, false}, {"latitude limite haute valide", Coordinates{Latitude: 90, Longitude: 0}, false}, {"latitude limite basse valide", Coordinates{Latitude: -90, Longitude: 0}, false}, {"latitude trop haute", Coordinates{Latitude: 90.1, Longitude: 0}, true}, {"latitude trop basse", Coordinates{Latitude: -90.1, Longitude: 0}, true}, {"longitude limite haute valide", Coordinates{Latitude: 0, Longitude: 180}, false}, {"longitude trop haute", Coordinates{Latitude: 0, Longitude: 180.1}, true}, {"longitude trop basse", Coordinates{Latitude: 0, Longitude: -180.1}, true}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { err := ValidateCoordinates(c.coords) if c.wantErr && err == nil { t.Error("attendu une erreur, reçu nil") } if !c.wantErr && err != nil { t.Errorf("erreur inattendue: %v", err) } }) } }