package tests import ( "bytes" "fmt" "net/http" "net/http/httptest" "testing" "gestion/handlers" "github.com/gin-gonic/gin" ) func cmdContext(method, username, role string, body []byte, commandID int) (*gin.Context, *httptest.ResponseRecorder) { var reader *bytes.Reader if body != nil { reader = bytes.NewReader(body) } else { reader = bytes.NewReader([]byte{}) } req := httptest.NewRequest(method, "/api/v1/commands", reader) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) c.Request = req c.Set("database", testDB) if username != "" { c.Set("username", username) } c.Set("role", role) if commandID != 0 { c.Params = gin.Params{{Key: "id", Value: fmt.Sprintf("%d", commandID)}} } return c, rec } // ── CancelCommandByClient (HTTP layer) ─────────────────────────────────── func TestCancelCommandByClient_RejectsNonClientRole(t *testing.T) { cleanupStockTestData(t) c, rec := cmdContext(http.MethodPost, testUserPrefix+"cancel_role", "admin", nil, 1) handlers.CancelCommandByClient(c) if rec.Code != http.StatusForbidden { t.Errorf("role admin doit être refusé: got=%d want=403", rec.Code) } } func TestCancelCommandByClient_InvalidCommandIDReturns400(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "cancel_badid") c, rec := cmdContext(http.MethodPost, username, "client", nil, 0) c.Params = gin.Params{{Key: "id", Value: "not-a-number"}} handlers.CancelCommandByClient(c) if rec.Code != http.StatusBadRequest { t.Errorf("ID invalide doit retourner 400: got=%d", rec.Code) } } func TestCancelCommandByClient_UnknownCommandReturns404(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "cancel_404") c, rec := cmdContext(http.MethodPost, username, "client", nil, 99999999) handlers.CancelCommandByClient(c) if rec.Code != http.StatusNotFound { t.Errorf("commande inconnue doit retourner 404: got=%d body=%s", rec.Code, rec.Body.String()) } } func TestCancelCommandByClient_WrongOwnerReturns403(t *testing.T) { cleanupStockTestData(t) owner := newTestClient(t, "cancel_owner") intruder := newTestClient(t, "cancel_intruder") productID := newTestProduct(t, "CancelWrongOwner", 10) cmdID := newTestCommandWithItem(t, owner, "pending", "", productID, 1, 10) c, rec := cmdContext(http.MethodPost, intruder, "client", nil, cmdID) handlers.CancelCommandByClient(c) if rec.Code != http.StatusForbidden { t.Errorf("un autre client ne doit pas pouvoir annuler: got=%d body=%s", rec.Code, rec.Body.String()) } } func TestCancelCommandByClient_SuccessNoPenaltyWhenNoLivreur(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "cancel_ok_nopenalty") productID := newTestProduct(t, "CancelOkNoPenalty", 10) cmdID := newTestCommandWithItem(t, username, "pending", "", productID, 1, 10) c, rec := cmdContext(http.MethodPost, username, "client", nil, cmdID) handlers.CancelCommandByClient(c) if rec.Code != http.StatusOK { t.Fatalf("annulation sans livreur doit réussir: got=%d body=%s", rec.Code, rec.Body.String()) } if got := commandStatus(t, cmdID); got != "cancelled" { t.Errorf("statut après annulation: got=%s want=cancelled", got) } } func TestCancelCommandByClient_TerminalStatusReturns400(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "cancel_terminal") productID := newTestProduct(t, "CancelTerminal", 10) cmdID := newTestCommandWithItem(t, username, "livre", "", productID, 1, 10) c, rec := cmdContext(http.MethodPost, username, "client", nil, cmdID) handlers.CancelCommandByClient(c) if rec.Code != http.StatusBadRequest { t.Errorf("annulation d'une commande livrée doit être rejetée: got=%d body=%s", rec.Code, rec.Body.String()) } } func TestCancelCommandByClient_ConfirmationRequiredReturns409WithPenaltyWarning(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "cancel_confirm") livreur := "cancel_confirm_livreur" productID := newTestProduct(t, "CancelConfirm", 10) cmdID := newTestCommandWithItem(t, username, "en_route", livreur, productID, 1, 10) if err := testDB.SetCommandETA(cmdID, 14); err != nil { t.Fatalf("SetCommandETA: %v", err) } c, rec := cmdContext(http.MethodPost, username, "client", nil, cmdID) handlers.CancelCommandByClient(c) if rec.Code != http.StatusConflict { t.Fatalf("annulation tardive sans force doit demander confirmation: got=%d body=%s", rec.Code, rec.Body.String()) } if !bytes.Contains(rec.Body.Bytes(), []byte(`"will_apply":true`)) { t.Errorf("la réponse doit avertir d'une pénalité à venir: body=%s", rec.Body.String()) } // Statut inchangé tant que non confirmé. if got := commandStatus(t, cmdID); got != "en_route" { t.Errorf("statut ne doit pas changer avant confirmation: got=%s want=en_route", got) } } // ── GetMyCancellationHistory ────────────────────────────────────────────── func TestGetMyCancellationHistory_RejectsNonClientRole(t *testing.T) { cleanupStockTestData(t) c, rec := cmdContext(http.MethodGet, testUserPrefix+"hist_role", "livreur", nil, 0) handlers.GetMyCancellationHistory(c) if rec.Code != http.StatusForbidden { t.Errorf("role livreur doit être refusé: got=%d want=403", rec.Code) } } func TestGetMyCancellationHistory_ReturnsHistoryAndTotalPenalties(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "hist_ok") c, rec := cmdContext(http.MethodGet, username, "client", nil, 0) handlers.GetMyCancellationHistory(c) if rec.Code != http.StatusOK { t.Fatalf("historique doit réussir pour un client: got=%d body=%s", rec.Code, rec.Body.String()) } if !bytes.Contains(rec.Body.Bytes(), []byte(`"total_penalties"`)) { t.Errorf("la réponse doit inclure total_penalties: body=%s", rec.Body.String()) } } // ── GetAllCancelledOrders ───────────────────────────────────────────────── func TestGetAllCancelledOrders_RejectsNonAdminNonCabine(t *testing.T) { cleanupStockTestData(t) c, rec := cmdContext(http.MethodGet, testUserPrefix+"allcancel_role", "livreur", nil, 0) handlers.GetAllCancelledOrders(c) if rec.Code != http.StatusForbidden { t.Errorf("role livreur doit être refusé: got=%d want=403", rec.Code) } } func TestGetAllCancelledOrders_InvalidLimitFallsBackToDefault(t *testing.T) { cleanupStockTestData(t) c, rec := cmdContext(http.MethodGet, testUserPrefix+"allcancel_limit", "admin", nil, 0) req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/cancelled?limit=not-a-number", nil) c.Request = req c.Set("database", testDB) c.Set("username", testUserPrefix+"allcancel_limit") c.Set("role", "admin") handlers.GetAllCancelledOrders(c) if rec.Code != http.StatusOK { t.Fatalf("limit invalide doit quand même réussir avec un défaut: got=%d body=%s", rec.Code, rec.Body.String()) } } func TestGetAllCancelledOrders_LimitCapsAt500(t *testing.T) { cleanupStockTestData(t) rec := httptest.NewRecorder() c, _ := gin.CreateTestContext(rec) c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/cancelled?limit=99999", nil) c.Set("database", testDB) c.Set("username", testUserPrefix+"allcancel_cap") c.Set("role", "admin") handlers.GetAllCancelledOrders(c) if rec.Code != http.StatusOK { t.Fatalf("limit énorme doit quand même réussir (plafonné): got=%d body=%s", rec.Code, rec.Body.String()) } } // ── DeleteCommandByCabine ───────────────────────────────────────────────── func TestDeleteCommandByCabine_RejectsNonCabineNonAdmin(t *testing.T) { cleanupStockTestData(t) c, rec := cmdContext(http.MethodDelete, testUserPrefix+"delcab_role", "client", nil, 1) handlers.DeleteCommandByCabine(c) if rec.Code != http.StatusForbidden { t.Errorf("role client doit être refusé: got=%d want=403", rec.Code) } } func TestDeleteCommandByCabine_UnknownCommandReturns404(t *testing.T) { cleanupStockTestData(t) c, rec := cmdContext(http.MethodDelete, testUserPrefix+"delcab_404", "cabine", nil, 99999999) handlers.DeleteCommandByCabine(c) if rec.Code != http.StatusNotFound { t.Errorf("commande inconnue doit retourner 404: got=%d body=%s", rec.Code, rec.Body.String()) } } func TestDeleteCommandByCabine_SuccessDeletesCommand(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "delcab_ok") productID := newTestProduct(t, "DelCabOk", 10) cmdID := newTestCommandWithItem(t, username, "pending", "", productID, 1, 10) c, rec := cmdContext(http.MethodDelete, testUserPrefix+"delcab_ok_actor", "cabine", nil, cmdID) handlers.DeleteCommandByCabine(c) if rec.Code != http.StatusOK { t.Fatalf("suppression par cabine doit réussir: got=%d body=%s", rec.Code, rec.Body.String()) } var count int64 testDB.GDB.Raw(`SELECT COUNT(*) FROM commandes WHERE id = ?`, cmdID).Scan(&count) if count != 0 { t.Errorf("la commande doit être supprimée: got=%d lignes restantes", count) } } // ── validateReason (testé indirectement via CancelCommandByClient) ─────── func TestCancelCommandByClient_BlankReasonDefaultsToStandardMessage(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "reason_blank") productID := newTestProduct(t, "ReasonBlank", 10) cmdID := newTestCommandWithItem(t, username, "pending", "", productID, 1, 10) body := []byte(`{"reason":""}`) c, rec := cmdContext(http.MethodPost, username, "client", body, cmdID) handlers.CancelCommandByClient(c) if rec.Code != http.StatusOK { t.Fatalf("annulation avec reason vide doit réussir (validateReason doit fournir un défaut, pas rejeter): got=%d body=%s", rec.Code, rec.Body.String()) } if got := commandStatus(t, cmdID); got != "cancelled" { t.Errorf("statut après annulation avec raison vide: got=%s want=cancelled", got) } }