Files
2026-09-16 20:21:41 +02:00

225 lines
12 KiB
Go

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)
}
}