package projects import ( "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "github.com/omnex/control-plane/api/internal/demos" ) func newTestRouter(t *testing.T) (*gin.Engine, *fakeProv) { t.Helper() gin.SetMode(gin.TestMode) svc, prov, _ := newTestService(t) h := NewHandler(svc) r := gin.New() r.POST("/projects", h.Create) r.GET("/projects", h.List) r.GET("/projects/:id", h.Get) r.DELETE("/projects/:id", h.Delete) r.POST("/projects/:id/extend", h.Extend) r.GET("/projects/:id/details", h.Details) return r, prov } func do(r *gin.Engine, method, path, body string) *httptest.ResponseRecorder { req := httptest.NewRequest(method, path, strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } const goodBody = `{"client_name":"Dupont","months":%s,"admin_username":"admin","admin_password":"un-mot-de-passe-solide"}` func body(months string) string { return strings.Replace(goodBody, "%s", months, 1) } func TestHandlerCreate_MonthsBounds(t *testing.T) { r, _ := newTestRouter(t) for _, months := range []string{"0", "13", "-1", `"x"`} { if w := do(r, "POST", "/projects", body(months)); w.Code != http.StatusBadRequest { t.Errorf("months=%s : HTTP %d, attendu 400", months, w.Code) } } for _, months := range []string{"1", "12"} { w := do(r, "POST", "/projects", body(months)) if w.Code != http.StatusAccepted { t.Errorf("months=%s : HTTP %d, attendu 202 (%s)", months, w.Code, w.Body) } } } func TestHandlerCreate_NeverEchoesAdminPassword(t *testing.T) { r, prov := newTestRouter(t) w := do(r, "POST", "/projects", body("2")) if w.Code != http.StatusAccepted { t.Fatalf("HTTP %d: %s", w.Code, w.Body) } <-prov.provisioned if strings.Contains(w.Body.String(), "un-mot-de-passe-solide") { t.Error("le mot de passe admin figure dans la réponse") } if list := do(r, "GET", "/projects", ""); strings.Contains(list.Body.String(), "un-mot-de-passe-solide") { t.Error("le mot de passe admin figure dans la liste") } } func TestHandlerCreate_RejectsWeakPasswordAndBadAdminName(t *testing.T) { r, _ := newTestRouter(t) for name, payload := range map[string]string{ "mot de passe court": `{"client_name":"D","months":1,"admin_username":"admin","admin_password":"court"}`, "admin non alphanum": `{"client_name":"D","months":1,"admin_username":"ad min","admin_password":"un-mot-de-passe-solide"}`, "client manquant": `{"months":1,"admin_username":"admin","admin_password":"un-mot-de-passe-solide"}`, "trop d'admins": `{"client_name":"D","months":1,"admin_username":"admin","admin_password":"un-mot-de-passe-solide","admin_number":21}`, } { if w := do(r, "POST", "/projects", payload); w.Code != http.StatusBadRequest { t.Errorf("%s : HTTP %d, attendu 400", name, w.Code) } } } func TestHandlerExtend_BoundsAndNotFound(t *testing.T) { r, _ := newTestRouter(t) if w := do(r, "POST", "/projects/inconnu/extend", `{"months":2}`); w.Code != http.StatusNotFound { t.Errorf("projet inconnu : HTTP %d, attendu 404", w.Code) } if w := do(r, "POST", "/projects/inconnu/extend", `{"months":13}`); w.Code != http.StatusBadRequest { t.Errorf("13 mois : HTTP %d, attendu 400", w.Code) } if w := do(r, "POST", "/projects/inconnu/extend", `{}`); w.Code != http.StatusBadRequest { t.Errorf("sans durée : HTTP %d, attendu 400", w.Code) } } func TestHandlerDetails(t *testing.T) { r, prov := newTestRouter(t) prov.state = demos.ResourceState{API: demos.ComponentState{Phase: "Running"}} if w := do(r, "GET", "/projects/inconnu/details", ""); w.Code != http.StatusNotFound { t.Errorf("projet inconnu : %d, attendu 404", w.Code) } w := do(r, "POST", "/projects", body("1")) if w.Code != http.StatusAccepted { t.Fatalf("Create: %d %s", w.Code, w.Body) } var created Project if err := json.Unmarshal(w.Body.Bytes(), &created); err != nil { t.Fatal(err) } <-prov.provisioned w = do(r, "GET", "/projects/"+created.ID+"/details", "") if w.Code != http.StatusOK { t.Fatalf("Details: %d %s", w.Code, w.Body) } var got struct { Namespace string `json:"namespace"` State demos.ResourceState `json:"state"` } if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { t.Fatal(err) } if got.Namespace != created.Namespace || got.State.API.Phase != "Running" { t.Errorf("réponse inattendue : %+v", got) } prov.stateErr = errors.New("api k8s indisponible") if w := do(r, "GET", "/projects/"+created.ID+"/details", ""); w.Code != http.StatusInternalServerError || strings.Contains(w.Body.String(), "k8s") { t.Errorf("erreur cluster : %d %s (500 sans détail interne attendu)", w.Code, w.Body) } }