package tests import ( "encoding/json" "net/http" "net/http/httptest" "testing" "gestion/handlers" "gestion/models" "github.com/gin-gonic/gin" ) func adminStatsContext() (*gin.Context, *httptest.ResponseRecorder) { req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/stats", nil) rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) c.Request = req c.Set("database", testDB) return c, rec } // insertStatsCommand insère une commande directement avec un created_at // choisi, pour contrôler précisément le jour de semaine/heure agrégés. func insertStatsCommand(t *testing.T, username, status, createdAt string) { t.Helper() testDB.GDB.Exec( `INSERT INTO commandes (username, status, adresse, total_prix, created_at, updated_at) VALUES (?, ?, 'Adresse stats test', 10, ?::timestamp, ?::timestamp)`, username, status, createdAt, createdAt, ) } func TestGetAdminStats_ZeroOrdersAvgPerDayIsZeroNoPanic(t *testing.T) { cleanupStockTestData(t) // Table commandes peut contenir des données d'autres tests, mais aucune // n'utilise ce username isolé — on vérifie juste l'absence de panic/NaN // et que la structure de réponse est bien formée avec les vraies // données actuelles de la base de test (qui peut être non-vide). c, rec := adminStatsContext() handlers.GetAdminStats(c) if rec.Code != http.StatusOK { t.Fatalf("GetAdminStats doit réussir: got=%d body=%s", rec.Code, rec.Body.String()) } var resp struct { Summary struct { AvgPerDay float64 `json:"avg_per_day"` } `json:"summary"` } if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("réponse JSON invalide: %v", err) } if resp.Summary.AvgPerDay < 0 { t.Errorf("avg_per_day ne doit jamais être négatif: got=%f", resp.Summary.AvgPerDay) } } func TestGetAdminStats_PeakWeekdayMatchesBusiestDay(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "stats_peak") // 2024-01-08 est un lundi, 2024-01-07 un dimanche (DOW Postgres: 0=dimanche). insertStatsCommand(t, username, "pending", "2024-01-08 10:00:00") insertStatsCommand(t, username, "pending", "2024-01-08 11:00:00") insertStatsCommand(t, username, "pending", "2024-01-08 12:00:00") insertStatsCommand(t, username, "pending", "2024-01-07 10:00:00") c, rec := adminStatsContext() handlers.GetAdminStats(c) if rec.Code != http.StatusOK { t.Fatalf("GetAdminStats doit réussir: got=%d body=%s", rec.Code, rec.Body.String()) } var resp struct { Summary struct { PeakWeekday string `json:"peak_weekday"` } `json:"summary"` } if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("réponse JSON invalide: %v", err) } if resp.Summary.PeakWeekday != "Lundi" { t.Errorf("peak_weekday doit être le jour avec le plus de commandes: got=%q want=Lundi", resp.Summary.PeakWeekday) } } func TestGetAdminStats_ByQuantitySortedDescendingByTotalOrders(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "stats_byqty") prodLow := newTestProduct(t, "ByQtyLow", 100) prodHigh := newTestProduct(t, "ByQtyHigh", 100) // prodHigh: 3 commandes ; prodLow: 1 commande. for range 3 { newTestCommandWithItem(t, username, "pending", "", prodHigh, 1, 10) } newTestCommandWithItem(t, username, "pending", "", prodLow, 1, 10) c, rec := adminStatsContext() handlers.GetAdminStats(c) if rec.Code != http.StatusOK { t.Fatalf("GetAdminStats doit réussir: got=%d body=%s", rec.Code, rec.Body.String()) } var resp struct { ByQuantity []struct { ProductID int `json:"product_id"` TotalOrders int `json:"total_orders"` } `json:"by_quantity"` } if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("réponse JSON invalide: %v", err) } // Vérifie l'ordre décroissant global (pas seulement nos deux produits, // d'autres tests peuvent avoir laissé des données) et que prodHigh // apparaît avant prodLow. highIdx, lowIdx := -1, -1 for i, r := range resp.ByQuantity { if r.ProductID == prodHigh { highIdx = i } if r.ProductID == prodLow { lowIdx = i } if i > 0 && r.TotalOrders > resp.ByQuantity[i-1].TotalOrders { t.Errorf("by_quantity doit être trié par total_orders décroissant: rupture à l'index %d", i) } } if highIdx == -1 || lowIdx == -1 { t.Fatalf("les deux produits de test doivent apparaître dans by_quantity: highIdx=%d lowIdx=%d", highIdx, lowIdx) } if highIdx >= lowIdx { t.Errorf("le produit avec le plus de commandes doit apparaître avant: highIdx=%d lowIdx=%d", highIdx, lowIdx) } } func TestOrdersAndRevenueByHour_CountsNonCancelledRevenueOnlyApproved(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "stats_hour") insertStatsCommand(t, username, "approved", "2024-01-08 14:00:00") insertStatsCommand(t, username, "pending", "2024-01-08 14:30:00") insertStatsCommand(t, username, "cancelled", "2024-01-08 14:45:00") testDB.GDB.Exec(`UPDATE commandes SET total_prix = 25 WHERE username = ? AND status = 'approved'`, username) var hourRows []models.HourRow if err := testDB.OrdersAndRevenueByHour(&hourRows, testDB.ReadResetAt("stats_reset_heures_at")); err != nil { t.Fatalf("OrdersAndRevenueByHour: %v", err) } var found bool for _, r := range hourRows { if r.Hour == 14 { found = true if r.Count != 2 { t.Errorf("count à 14h doit exclure la commande annulée: got=%d want=2", r.Count) } if r.Revenue != 25 { t.Errorf("revenue à 14h ne doit compter que les commandes approuvées: got=%.2f want=25", r.Revenue) } } } if !found { t.Fatal("aucune ligne pour l'heure 14 trouvée") } }