package web import ( "encoding/json" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "gardomatic.kleiax.de/lib/client" "github.com/julienschmidt/httprouter" ) func TestPlantAndLocationCanBeEdited(t *testing.T) { var plantInput client.PlantInput var locationInput client.LocationInput assignmentUpdated := false apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == http.MethodPatch && r.URL.Path == "/v1/gardens/3/plants/5": body, _ := io.ReadAll(r.Body) if err := json.Unmarshal(body, &plantInput); err != nil { t.Fatal(err) } _, _ = w.Write([]byte(`{"plant":{"id":5,"garden_id":3,"name":"Neue Rose"}}`)) case r.Method == http.MethodPatch && r.URL.Path == "/v1/gardens/3/plants/5/locations/9": assignmentUpdated = true _, _ = w.Write([]byte(`{"plant_location":{"id":9,"plant_id":5,"location_id":6,"quantity":2}}`)) case r.Method == http.MethodPatch && r.URL.Path == "/v1/gardens/3/locations/6": body, _ := io.ReadAll(r.Body) if err := json.Unmarshal(body, &locationInput); err != nil { t.Fatal(err) } _, _ = w.Write([]byte(`{"location":{"id":6,"garden_id":3,"name":"Südbeet"}}`)) default: http.NotFound(w, r) } }) app := newAPIBackedTestApplication(t, apiHandler) plantForm := url.Values{"name": {"Neue Rose"}, "species_id": {"0"}, "location_id": {"6"}, "assignment_id": {"9"}, "quantity": {"2"}, "status": {"active"}} plantRequest := httptest.NewRequest(http.MethodPost, "/g/3/plants/edit/5", strings.NewReader(plantForm.Encode())) plantRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded") plantRequest = taskWebRequest(plantRequest, app.apiClient, httprouter.Params{{Key: "gardenID", Value: "3"}, {Key: "plantID", Value: "5"}}) plantResponse := httptest.NewRecorder() app.plantEditPost(plantResponse, plantRequest) if plantResponse.Code != http.StatusSeeOther || !plantInput.ClearSpeciesID || !assignmentUpdated { t.Fatalf("plant edit: status=%d input=%+v assignment=%v body=%s", plantResponse.Code, plantInput, assignmentUpdated, plantResponse.Body.String()) } locationForm := url.Values{"name": {"Südbeet"}, "parent_id": {"0"}, "area_sqm": {"4,5"}, "return_to": {"/g/3/locations"}} locationRequest := httptest.NewRequest(http.MethodPost, "/g/3/locations/edit/6", strings.NewReader(locationForm.Encode())) locationRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded") locationRequest = taskWebRequest(locationRequest, app.apiClient, httprouter.Params{{Key: "gardenID", Value: "3"}, {Key: "locationID", Value: "6"}}) locationResponse := httptest.NewRecorder() app.locationEditPost(locationResponse, locationRequest) if locationResponse.Code != http.StatusSeeOther || !locationInput.ClearParentID || locationInput.AreaSQM == nil || *locationInput.AreaSQM != 4.5 { t.Fatalf("location edit: status=%d input=%+v body=%s", locationResponse.Code, locationInput, locationResponse.Body.String()) } } func TestGardenAndSpeciesCanBeEdited(t *testing.T) { var gardenInput client.UpdateGardenInput var speciesInput client.SpeciesInput apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == http.MethodPatch && r.URL.Path == "/v1/gardens/3": body, _ := io.ReadAll(r.Body) if err := json.Unmarshal(body, &gardenInput); err != nil { t.Fatal(err) } _, _ = w.Write([]byte(`{"garden":{"id":3,"name":"Neuer Garten"}}`)) case r.Method == http.MethodPatch && r.URL.Path == "/v1/gardens/3/species/7": body, _ := io.ReadAll(r.Body) if err := json.Unmarshal(body, &speciesInput); err != nil { t.Fatal(err) } _, _ = w.Write([]byte(`{"species":{"id":7,"garden_id":3,"common_name":"Neue Rose"}}`)) default: http.NotFound(w, r) } }) app := newAPIBackedTestApplication(t, apiHandler) gardenForm := url.Values{"name": {"Neuer Garten"}, "description": {"Südseite"}} gardenRequest := httptest.NewRequest(http.MethodPost, "/gardens/edit/3", strings.NewReader(gardenForm.Encode())) gardenRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded") gardenRequest = taskWebRequest(gardenRequest, app.apiClient, httprouter.Params{{Key: "gardenID", Value: "3"}}) gardenResponse := httptest.NewRecorder() app.gardenEditPost(gardenResponse, gardenRequest) if gardenResponse.Code != http.StatusSeeOther || gardenInput.Name == nil || *gardenInput.Name != "Neuer Garten" { t.Fatalf("garden edit: status=%d input=%+v", gardenResponse.Code, gardenInput) } speciesForm := url.Values{"common_name": {"Neue Rose"}, "height_cm_from": {"40"}, "height_cm_to": {"80"}, "width_cm_to": {"60"}, "sow_month_from": {"0"}, "sow_month_to": {"0"}} speciesRequest := httptest.NewRequest(http.MethodPost, "/g/3/species/edit/7", strings.NewReader(speciesForm.Encode())) speciesRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded") speciesRequest = taskWebRequest(speciesRequest, app.apiClient, httprouter.Params{{Key: "gardenID", Value: "3"}, {Key: "speciesID", Value: "7"}}) speciesResponse := httptest.NewRecorder() app.speciesSave(speciesResponse, speciesRequest) if speciesResponse.Code != http.StatusSeeOther || speciesInput.CommonName == nil || *speciesInput.CommonName != "Neue Rose" || !speciesInput.ClearSowRange { t.Fatalf("species edit: status=%d input=%+v", speciesResponse.Code, speciesInput) } if speciesInput.HeightCMFrom == nil || *speciesInput.HeightCMFrom != 40 || speciesInput.HeightCMTo == nil || *speciesInput.HeightCMTo != 80 || speciesInput.WidthCMTo == nil || *speciesInput.WidthCMTo != 60 { t.Fatalf("species dimensions were not submitted: %+v", speciesInput) } if speciesInput.ClearHeightCMFrom || speciesInput.ClearHeightCMTo || !speciesInput.ClearWidthCMFrom || speciesInput.ClearWidthCMTo { t.Fatalf("species dimension clear flags are incorrect: %+v", speciesInput) } } func TestGardenCanBeDeleted(t *testing.T) { deleted := false apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete || r.URL.Path != "/v1/gardens/3" { http.NotFound(w, r) return } deleted = true w.WriteHeader(http.StatusNoContent) }) app := newAPIBackedTestApplication(t, apiHandler) request := httptest.NewRequest(http.MethodPost, "/gardens/delete/3", nil) request = taskWebRequest(request, app.apiClient, httprouter.Params{{Key: "gardenID", Value: "3"}}) response := httptest.NewRecorder() app.gardenDeletePost(response, request) if response.Code != http.StatusSeeOther || response.Header().Get("Location") != "/gardens" || !deleted { t.Fatalf("garden delete: status=%d location=%q deleted=%v body=%s", response.Code, response.Header().Get("Location"), deleted, response.Body.String()) } } func TestTaskTemplateCanBeCreated(t *testing.T) { var input client.SpeciesTaskTemplateInput apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.Method != http.MethodPost || r.URL.Path != "/v1/gardens/3/species/7/task-templates" { http.NotFound(w, r) return } body, _ := io.ReadAll(r.Body) if err := json.Unmarshal(body, &input); err != nil { t.Fatal(err) } w.WriteHeader(http.StatusCreated) _, _ = w.Write([]byte(`{"task_template":{"id":9,"species_id":7,"title":"Schneiden"}}`)) }) app := newAPIBackedTestApplication(t, apiHandler) form := url.Values{"title": {"Schneiden"}, "trigger_type": {"month_of_year"}, "month_from": {"11"}, "day_from": {"15"}, "duration": {"2"}, "duration_unit": {"week"}, "recurrence": {"monthly"}, "recurrence_interval": {"2"}, "priority": {"5"}, "active": {"true"}} request := httptest.NewRequest(http.MethodPost, "/g/3/task-templates/new?species_id=7", strings.NewReader(form.Encode())) request.Header.Set("Content-Type", "application/x-www-form-urlencoded") request = taskWebRequest(request, app.apiClient, httprouter.Params{{Key: "gardenID", Value: "3"}}) response := httptest.NewRecorder() app.taskTemplateSave(response, request) if response.Code != http.StatusSeeOther || input.TriggerType == nil || *input.TriggerType != "month_of_year" || input.MonthFrom == nil || *input.MonthFrom != 11 || input.DayFrom == nil || *input.DayFrom != 15 || input.Duration == nil || *input.Duration != 2 || input.RecurrenceInterval == nil || *input.RecurrenceInterval != 2 { t.Fatalf("template create: status=%d input=%+v body=%s", response.Code, input, response.Body.String()) } if input.TriggerOffset != nil || input.TriggerOffsetUnit != nil { t.Errorf("calendar template unexpectedly sends disabled relative fields: %+v", input) } } func TestTaskTemplateDialogShowsMissingNameError(t *testing.T) { apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == http.MethodGet && r.URL.Path == "/v1/gardens/3": _, _ = w.Write([]byte(`{"garden":{"id":3,"name":"Hinterhof","role":"owner"}}`)) case r.Method == http.MethodGet && r.URL.Path == "/v1/gardens/3/species/7": _, _ = w.Write([]byte(`{"species":{"id":7,"common_name":"Tomate"}}`)) case r.Method == http.MethodGet && r.URL.Path == "/v1/task-priorities": _, _ = w.Write([]byte(`{"priorities":[{"id":1,"name":"Normal","value":0,"active":true}]}`)) default: t.Errorf("unexpected API request: %s %s", r.Method, r.URL.Path) http.NotFound(w, r) } }) app := newAPIBackedTestApplication(t, apiHandler) form := url.Values{"trigger_type": {"month_of_year"}, "month_from": {"9"}, "day_from": {"1"}, "duration": {"0"}, "duration_unit": {"day"}, "trigger_offset_unit": {"day"}, "recurrence_interval": {"1"}} request := httptest.NewRequest(http.MethodPost, "/g/3/task-templates/new?species_id=7", strings.NewReader(form.Encode())) request.Header.Set("Content-Type", "application/x-www-form-urlencoded") request.Header.Set("HX-Request", "true") request = taskWebRequest(request, app.apiClient, httprouter.Params{{Key: "gardenID", Value: "3"}}) response := httptest.NewRecorder() app.taskTemplateSave(response, request) if response.Code != http.StatusOK { t.Fatalf("status: got %d, want %d; body: %s", response.Code, http.StatusOK, response.Body.String()) } if body := response.Body.String(); !strings.Contains(body, "Ein Name ist erforderlich.") { t.Errorf("missing name validation message: %s", body) } if response.Header().Get("HX-Retarget") != "#task-template-dialog-host" { t.Errorf("HX-Retarget: got %q", response.Header().Get("HX-Retarget")) } form.Set("title", "Ausgegeizte Triebe entfernen") form.Set("month_from", "0") request = httptest.NewRequest(http.MethodPost, "/g/3/task-templates/new?species_id=7", strings.NewReader(form.Encode())) request.Header.Set("Content-Type", "application/x-www-form-urlencoded") request.Header.Set("HX-Request", "true") request = taskWebRequest(request, app.apiClient, httprouter.Params{{Key: "gardenID", Value: "3"}}) response = httptest.NewRecorder() app.taskTemplateSave(response, request) body := response.Body.String() if response.Code != http.StatusOK { t.Fatalf("status with filled name: got %d, want %d; body: %s", response.Code, http.StatusOK, body) } if !strings.Contains(body, "value='Ausgegeizte Triebe entfernen'") { t.Errorf("filled name was not preserved: %s", body) } if strings.Contains(body, "Ein Name ist erforderlich.") { t.Errorf("filled name was incorrectly rejected: %s", body) } if !strings.Contains(body, "Tag und Monat für „Ab“ sind erforderlich.") { t.Errorf("actual date validation message is missing: %s", body) } }