This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user