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 TestPlantListUsesClickableCardsAndStatusMenu(t *testing.T) { apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v1/gardens/3": _, _ = w.Write([]byte(`{"garden":{"id":3,"name":"Hinterhof","role":"owner"}}`)) case "/v1/gardens/3/plants": _, _ = w.Write([]byte(`{"plants":[{"id":5,"garden_id":3,"name":"Rose","status":"active"}]}`)) case "/v1/gardens/3/species": _, _ = w.Write([]byte(`{"species":[]}`)) case "/v1/gardens/3/locations": _, _ = w.Write([]byte(`{"locations":[]}`)) case "/v1/gardens/3/plants/5/locations": _, _ = w.Write([]byte(`{"plant_locations":[]}`)) default: http.NotFound(w, r) } }) app := newAPIBackedTestApplication(t, apiHandler) request := httptest.NewRequest(http.MethodGet, "/g/3/plants", nil) request = taskWebRequest(request, app.apiClient, httprouter.Params{{Key: "gardenID", Value: "3"}}) response := httptest.NewRecorder() app.plants(response, request) body := response.Body.String() for _, want := range []string{"card--clickable plant-card", "class='card-link' href='/g/3/plants/edit/5'", "plant-card-menu", "/g/3/plants/status/5"} { if !strings.Contains(body, want) { t.Errorf("plant list does not contain %q: %s", want, body) } } for _, unwanted := range []string{"Weiteren Ort zuordnen", ">Bearbeiten", ">Löschen"} { if strings.Contains(body, unwanted) { t.Errorf("plant list still contains %q: %s", unwanted, body) } } } func TestPlantEditRendersAllAssignmentsAndDeleteAction(t *testing.T) { apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v1/gardens/3": _, _ = w.Write([]byte(`{"garden":{"id":3,"name":"Hinterhof","role":"owner"}}`)) case "/v1/gardens/3/plants/5": _, _ = w.Write([]byte(`{"plant":{"id":5,"garden_id":3,"name":"Rose","status":"active"}}`)) case "/v1/gardens/3/plants/5/locations": _, _ = w.Write([]byte(`{"plant_locations":[{"id":9,"plant_id":5,"location_id":6,"quantity":2},{"id":10,"plant_id":5,"location_id":7,"quantity":4}]}`)) case "/v1/gardens/3/species": _, _ = w.Write([]byte(`{"species":[]}`)) case "/v1/gardens/3/locations": _, _ = w.Write([]byte(`{"locations":[{"id":6,"name":"Südbeet"},{"id":7,"name":"Topf"}]}`)) case "/v1/task-priorities": _, _ = w.Write([]byte(`{"priorities":[{"id":1,"name":"Normal","value":0,"active":true},{"id":2,"name":"Erhöht","value":3,"active":true}]}`)) default: http.NotFound(w, r) } }) app := newAPIBackedTestApplication(t, apiHandler) request := httptest.NewRequest(http.MethodGet, "/g/3/plants/edit/5", nil) request = taskWebRequest(request, app.apiClient, httprouter.Params{{Key: "gardenID", Value: "3"}, {Key: "plantID", Value: "5"}}) response := httptest.NewRecorder() app.plantEdit(response, request) body := response.Body.String() if strings.Count(body, "data-assignment-row") != 3 { // two rows plus the template row t.Fatalf("expected both assignments and the add-row template: %s", body) } for _, want := range []string{"value='9'", "value='10'", "data-add-assignment", "Pflanze löschen", "/g/3/plants/delete/5"} { if !strings.Contains(body, want) { t.Errorf("plant form does not contain %q: %s", want, body) } } } func TestPlantSaveUpdatesCreatesAndDeletesAssignments(t *testing.T) { var updated, created, deleted bool apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodPatch && r.URL.Path == "/v1/gardens/3/plants/5": _, _ = w.Write([]byte(`{"plant":{"id":5,"garden_id":3,"name":"Rose"}}`)) case r.Method == http.MethodPatch && r.URL.Path == "/v1/gardens/3/plants/5/locations/9": updated = true _, _ = w.Write([]byte(`{"plant_location":{"id":9}}`)) case r.Method == http.MethodPost && r.URL.Path == "/v1/gardens/3/plants/5/locations": created = true _, _ = w.Write([]byte(`{"plant_location":{"id":11}}`)) case r.Method == http.MethodDelete && r.URL.Path == "/v1/gardens/3/plants/5/locations/10": deleted = true w.WriteHeader(http.StatusNoContent) default: http.NotFound(w, r) } }) app := newAPIBackedTestApplication(t, apiHandler) form := url.Values{ "name": {"Rose"}, "species_id": {"0"}, "status": {"active"}, "assignment_id": {"9", "0", "10"}, "location_id": {"6", "7", "0"}, "quantity": {"2", "3", "1"}, } request := httptest.NewRequest(http.MethodPost, "/g/3/plants/edit/5", 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"}, {Key: "plantID", Value: "5"}}) response := httptest.NewRecorder() app.plantEditPost(response, request) if response.Code != http.StatusSeeOther || !updated || !created || !deleted { t.Fatalf("status=%d updated=%v created=%v deleted=%v body=%s", response.Code, updated, created, deleted, response.Body.String()) } } func TestPlantStatusPostOnlyUpdatesStatus(t *testing.T) { var input client.PlantInput apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) if err := json.Unmarshal(body, &input); err != nil { t.Fatal(err) } _, _ = w.Write([]byte(`{"plant":{"id":5,"status":"dormant"}}`)) }) app := newAPIBackedTestApplication(t, apiHandler) request := httptest.NewRequest(http.MethodPost, "/g/3/plants/status/5", strings.NewReader("status=dormant")) request.Header.Set("Content-Type", "application/x-www-form-urlencoded") request = taskWebRequest(request, app.apiClient, httprouter.Params{{Key: "gardenID", Value: "3"}, {Key: "plantID", Value: "5"}}) response := httptest.NewRecorder() app.plantStatusPost(response, request) if response.Code != http.StatusSeeOther || input.Status == nil || *input.Status != "dormant" || input.Name != nil { t.Fatalf("status=%d input=%+v body=%s", response.Code, input, response.Body.String()) } } func TestPlantTaskFragmentsAndAutomaticNameHooks(t *testing.T) { apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v1/gardens/3": _, _ = w.Write([]byte(`{"garden":{"id":3,"name":"Hinterhof","role":"owner"}}`)) case "/v1/gardens/3/species": _, _ = w.Write([]byte(`{"species":[{"id":7,"common_name":"Tomate","cultivar":"Roma"}]}`)) case "/v1/gardens/3/locations": _, _ = w.Write([]byte(`{"locations":[{"id":11,"garden_id":3,"name":"Gewächshaus"}]}`)) case "/v1/gardens/3/plants": _, _ = w.Write([]byte(`{"plants":[]}`)) case "/v1/gardens/3/tags": _, _ = w.Write([]byte(`{"tags":["Frühjahr"]}`)) case "/v1/task-priorities": _, _ = w.Write([]byte(`{"priorities":[{"id":1,"name":"Normal","value":0,"active":true},{"id":2,"name":"Erhöht","value":3,"active":true}]}`)) case "/v1/gardens/3/species/7/task-templates": _, _ = w.Write([]byte(`{"task_templates":[{"id":9,"species_id":7,"title":"Ausgeizen","active":true}]}`)) case "/v1/gardens/3/species/7/task-templates/9": _, _ = w.Write([]byte(`{"task_template":{"id":9,"species_id":7,"title":"Ausgeizen","description":"Seitentriebe entfernen","trigger_type":"month_of_year","month_from":5,"month_to":8,"active":true}}`)) default: http.NotFound(w, r) } }) app := newAPIBackedTestApplication(t, apiHandler) params := httprouter.Params{{Key: "gardenID", Value: "3"}} request := taskWebRequest(httptest.NewRequest(http.MethodGet, "/g/3/plants/new", nil), app.apiClient, params) response := httptest.NewRecorder() app.plantCreate(response, request) body := response.Body.String() for _, want := range []string{"data-plant-name", "data-plant-species", "data-plant-name-value='Tomate · Roma'", "/g/3/plant-template-tasks", "/g/3/plant-task-row"} { if !strings.Contains(body, want) { t.Errorf("plant form does not contain %q: %s", want, body) } } fragmentRequest := taskWebRequest(httptest.NewRequest(http.MethodGet, "/g/3/plant-template-tasks?species_id=7", nil), app.apiClient, params) fragmentResponse := httptest.NewRecorder() app.plantTemplateTasks(fragmentResponse, fragmentRequest) if fragmentResponse.Code != http.StatusOK || !strings.Contains(fragmentResponse.Body.String(), "Ausgeizen") || !strings.Contains(fragmentResponse.Body.String(), "plant_template_active_9") { t.Fatalf("template fragment: status=%d body=%s", fragmentResponse.Code, fragmentResponse.Body.String()) } rowRequest := taskWebRequest(httptest.NewRequest(http.MethodGet, "/g/3/plant-task-row?new=true&name=Jungpflanze&location_id=11", nil), app.apiClient, params) rowResponse := httptest.NewRecorder() app.plantTaskRow(rowResponse, rowRequest) rowBody := rowResponse.Body.String() for _, want := range []string{ "plant_task_title", "plant_task_description", "plant_task_keywords", "plant_task_plant_id", "plant_task_location_id", "plant_task_due_at_start", "plant_task_due_at_end", "plant_task_recurrence_interval", "plant_task_recurrence", "plant_task_priority", "plant_task_status_on_completion", "Jungpflanze · wird beim Speichern angelegt", "value='11' selected", "data-tag-suggestion='Frühjahr'", } { if !strings.Contains(rowBody, want) { t.Errorf("plant task dialog does not contain %q: %s", want, rowBody) } } if rowResponse.Code != http.StatusOK || !strings.Contains(rowBody, "plant_task_active") { t.Fatalf("task row fragment: status=%d body=%s", rowResponse.Code, rowResponse.Body.String()) } viewRequest := taskWebRequest(httptest.NewRequest(http.MethodGet, "/g/3/plant-template-task/9?species_id=7", nil), app.apiClient, httprouter.Params{{Key: "gardenID", Value: "3"}, {Key: "templateID", Value: "9"}}) viewResponse := httptest.NewRecorder() app.plantTemplateTaskView(viewResponse, viewRequest) if viewResponse.Code != http.StatusOK || !strings.Contains(viewResponse.Body.String(), "Anbinden") || !strings.Contains(body, "class='switch'") || strings.Contains(body, "