package http import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/go-chi/chi/v5" "github.com/go-playground/validator/v10" ) // newLandscapeTestHandler builds a handler with a nil service: every case below // is rejected while parsing or validating the request, before the service is // reached. func newLandscapeTestHandler() *LandscapeHandler { return NewLandscapeHandler(nil, validator.New(validator.WithRequiredStructEnabled())) } func postRun(t *testing.T, body string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodPost, "/landscape/runs", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() newLandscapeTestHandler().createRun(rec, req) return rec } func TestCreateRunInvalidJSON(t *testing.T) { rec := postRun(t, `{"from_stage":`) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d (body %s)", rec.Code, http.StatusBadRequest, rec.Body.String()) } } func TestCreateRunUnknownField(t *testing.T) { // DecodeJSON rejects unknown fields, so a typo is a 400 rather than a // silently ignored option. rec := postRun(t, `{"from_stages":1}`) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d (body %s)", rec.Code, http.StatusBadRequest, rec.Body.String()) } } func TestCreateRunFromStageOutOfRange(t *testing.T) { rec := postRun(t, `{"from_stage":7}`) if rec.Code != http.StatusUnprocessableEntity { t.Fatalf("status = %d, want %d (body %s)", rec.Code, http.StatusUnprocessableEntity, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "FromStage") { t.Fatalf("body %s does not name the offending field", rec.Body.String()) } } func TestCreateRunOverrideNotUUID(t *testing.T) { rec := postRun(t, `{"overrides":{"lp-z-lcr":"not-a-uuid"}}`) if rec.Code != http.StatusUnprocessableEntity { t.Fatalf("status = %d, want %d (body %s)", rec.Code, http.StatusUnprocessableEntity, rec.Body.String()) } } func TestUploadSlotRequiresMultipart(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/landscape/slots/lp-z-lcr", strings.NewReader(`{}`)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() newLandscapeTestHandler().uploadSlot(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d (body %s)", rec.Code, http.StatusBadRequest, rec.Body.String()) } } func TestListRunsInvalidPage(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/landscape/runs?page=0", nil) rec := httptest.NewRecorder() newLandscapeTestHandler().listRuns(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d (body %s)", rec.Code, http.StatusBadRequest, rec.Body.String()) } } func TestGetRunInvalidID(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/landscape/runs/nope", nil) rec := httptest.NewRecorder() newLandscapeTestHandler().getRun(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d (body %s)", rec.Code, http.StatusBadRequest, rec.Body.String()) } } // TestRegisterMountsRoutes checks the route table itself: every path reaches a // handler (405/404 would mean it is not mounted). Each request is one that is // rejected before the service is used. func TestRegisterMountsRoutes(t *testing.T) { r := chi.NewRouter() r.Route("/landscape", newLandscapeTestHandler().Register) cases := []struct { method, path, body, contentType string want int }{ {http.MethodPost, "/landscape/runs", `{`, "application/json", http.StatusBadRequest}, {http.MethodPost, "/landscape/slots/lp-z-lcr", `{}`, "application/json", http.StatusBadRequest}, {http.MethodGet, "/landscape/runs?page=0", "", "", http.StatusBadRequest}, {http.MethodGet, "/landscape/runs/nope", "", "", http.StatusBadRequest}, } for _, c := range cases { req := httptest.NewRequest(c.method, c.path, strings.NewReader(c.body)) if c.contentType != "" { req.Header.Set("Content-Type", c.contentType) } rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != c.want { t.Errorf("%s %s: status = %d, want %d (body %s)", c.method, c.path, rec.Code, c.want, rec.Body.String()) } } }