package tests import ( "fmt" "gestion/db" "strings" "testing" "time" ) const notificationsScheduledKey = "notifications:scheduled" // ScheduleETANotifications programme un rappel 5min et 3min avant l'arrivée // estimée — mais seulement si l'ETA le justifie (voir bornes ci-dessous). func TestScheduleETANotifications_SchedulesBothForLongETA(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "eta_notif_long") productID := newTestProduct(t, "EtaNotifLong", 10) cmdID := newTestCommandWithItem(t, username, "en_route", "", productID, 1, 10) before := time.Now() if err := testDB.ScheduleETANotifications(cmdID, 10); err != nil { t.Fatalf("ScheduleETANotifications: %v", err) } score5, err := getScheduledScore(t, cmdID, "5min") if err != nil { t.Fatalf("notification 5min absente: %v", err) } score3, err := getScheduledScore(t, cmdID, "3min") if err != nil { t.Fatalf("notification 3min absente: %v", err) } wantAt5 := before.Add(5 * time.Minute).Unix() // arrivée dans 10min - 5min = dans 5min wantAt3 := before.Add(7 * time.Minute).Unix() // arrivée dans 10min - 3min = dans 7min if diff := abs64(score5 - wantAt5); diff > 2 { t.Errorf("score notification 5min: got=%d want≈%d (écart %ds)", score5, wantAt5, diff) } if diff := abs64(score3 - wantAt3); diff > 2 { t.Errorf("score notification 3min: got=%d want≈%d (écart %ds)", score3, wantAt3, diff) } if score3 <= score5 { t.Errorf("la notification 3min doit être programmée après la 5min (plus proche de l'arrivée): score5=%d score3=%d", score5, score3) } } // À la limite exacte (ETA = 5 min), un rappel "5 minutes avant l'arrivée" // se déclencherait immédiatement (redondant) — il n'est donc volontairement // pas programmé (condition stricte ">5", pas ">=5"). Seul le rappel 3min // reste pertinent. func TestScheduleETANotifications_ExactlyFiveMinutes_SkipsFiveMinReminder(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "eta_notif_five") productID := newTestProduct(t, "EtaNotifFive", 10) cmdID := newTestCommandWithItem(t, username, "en_route", "", productID, 1, 10) if err := testDB.ScheduleETANotifications(cmdID, 5); err != nil { t.Fatalf("ScheduleETANotifications: %v", err) } if _, err := getScheduledScore(t, cmdID, "5min"); err == nil { t.Error("aucune notification 5min ne doit être programmée quand ETA=5min exactement") } if _, err := getScheduledScore(t, cmdID, "3min"); err != nil { t.Errorf("la notification 3min doit être programmée quand ETA=5min: %v", err) } } // À ETA = 3 min, même le rappel "3 minutes avant" serait immédiat : aucune // notification ne doit être programmée. func TestScheduleETANotifications_ExactlyThreeMinutes_SchedulesNothing(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "eta_notif_three") productID := newTestProduct(t, "EtaNotifThree", 10) cmdID := newTestCommandWithItem(t, username, "en_route", "", productID, 1, 10) if err := testDB.ScheduleETANotifications(cmdID, 3); err != nil { t.Fatalf("ScheduleETANotifications: %v", err) } if _, err := getScheduledScore(t, cmdID, "5min"); err == nil { t.Error("aucune notification 5min ne doit être programmée pour ETA=3min") } if _, err := getScheduledScore(t, cmdID, "3min"); err == nil { t.Error("aucune notification 3min ne doit être programmée pour ETA=3min (serait immédiate)") } } // Une ETA très courte (MinETA=3min, plancher de tout le système) ne doit // jamais programmer de notification de rappel — cohérent avec le cas ci-dessus. func TestScheduleETANotifications_VeryShortETASchedulesNothing(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "eta_notif_short") productID := newTestProduct(t, "EtaNotifShort", 10) cmdID := newTestCommandWithItem(t, username, "en_route", "", productID, 1, 10) if err := testDB.ScheduleETANotifications(cmdID, 1); err != nil { t.Fatalf("ScheduleETANotifications: %v", err) } if _, err := getScheduledScore(t, cmdID, "5min"); err == nil { t.Error("aucune notification ne doit être programmée pour une ETA d'1 minute") } if _, err := getScheduledScore(t, cmdID, "3min"); err == nil { t.Error("aucune notification ne doit être programmée pour une ETA d'1 minute") } } // SetCommandETA (appelée par UpdateDeliveryStatus au passage en "en_route") // déclenche automatiquement ScheduleETANotifications — vérifie l'intégration // complète, pas seulement la fonction isolée. func TestSetCommandETA_TriggersScheduledNotifications(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "eta_notif_integration") productID := newTestProduct(t, "EtaNotifIntegration", 10) cmdID := newTestCommandWithItem(t, username, "en_route", "", productID, 1, 10) if err := testDB.SetCommandETA(cmdID, 15); err != nil { t.Fatalf("SetCommandETA: %v", err) } if _, err := getScheduledScore(t, cmdID, "5min"); err != nil { t.Errorf("SetCommandETA doit programmer une notification 5min: %v", err) } if _, err := getScheduledScore(t, cmdID, "3min"); err != nil { t.Errorf("SetCommandETA doit programmer une notification 3min: %v", err) } } // Le message envoyé au client doit contenir une indication de temps lisible, // pas juste le statut brut. func TestSendETANotification_MessageContainsReadableTime(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "eta_notif_message") productID := newTestProduct(t, "EtaNotifMessage", 10) cmdID := newTestCommandWithItem(t, username, "en_route", "", productID, 1, 10) channel := fmt.Sprintf("notifications:command:%d", cmdID) pubsub := db.Redis.Subscribe(db.RedisCtx, channel) defer pubsub.Close() // Consommer le message de confirmation d'abonnement avant de publier. if _, err := pubsub.Receive(db.RedisCtx); err != nil { t.Fatalf("abonnement pubsub: %v", err) } testDB.SendETANotification(cmdID, "5min") select { case msg := <-pubsub.Channel(): if msg.Payload == "" { t.Fatal("message de notification vide") } if !strings.Contains(msg.Payload, "5min") { t.Errorf("le message doit indiquer le temps restant (%q): %q", "5min", msg.Payload) } t.Logf("message reçu: %q", msg.Payload) case <-time.After(3 * time.Second): t.Fatal("aucun message reçu sur le canal de notification dans le délai imparti") } } func abs64(n int64) int64 { if n < 0 { return -n } return n } // getScheduledScore lit le score (timestamp Unix) d'une notification // programmée pour "{commandID}:{suffix}" dans le sorted set Redis. func getScheduledScore(t *testing.T, commandID int, suffix string) (int64, error) { t.Helper() member := fmt.Sprintf("%d:%s", commandID, suffix) score, err := db.Redis.ZScore(db.RedisCtx, notificationsScheduledKey, member).Result() if err != nil { return 0, err } return int64(score), nil }