277 lines
14 KiB
Go
277 lines
14 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 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</a>", ">Löschen</button>"} {
|
|
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(), "<dialog") || !strings.Contains(viewResponse.Body.String(), "Seitentriebe entfernen") || strings.Contains(viewResponse.Body.String(), "<form") {
|
|
t.Fatalf("template detail fragment: status=%d body=%s", viewResponse.Code, viewResponse.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestPlantTaskDialogReturnsCompactEditableRow(t *testing.T) {
|
|
app := newAPIBackedTestApplication(t, http.NotFoundHandler())
|
|
form := url.Values{"new": {"true"}, "plant_task_row_key": {"task-new-1"}, "plant_task_id": {"0"}, "plant_task_title": {"Anbinden"}, "plant_task_description": {"Locker befestigen"}, "plant_task_priority": {"3"}, "plant_task_active": {"true"}}
|
|
request := httptest.NewRequest(http.MethodPost, "/g/3/plant-task-row", 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.plantTaskRowPost(response, request)
|
|
body := response.Body.String()
|
|
if response.Code != http.StatusOK || response.Header().Get("HX-Trigger") != "plantTaskSaved" || !strings.Contains(body, ">Anbinden</button>") || !strings.Contains(body, "class='switch'") || strings.Contains(body, "<textarea") {
|
|
t.Fatalf("task row fragment: status=%d trigger=%q body=%s", response.Code, response.Header().Get("HX-Trigger"), body)
|
|
}
|
|
}
|
|
|
|
func TestPlantCreateSavesManualTaskAndTemplateState(t *testing.T) {
|
|
var taskInput client.TaskInput
|
|
var optedOut bool
|
|
apiHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch {
|
|
case r.Method == http.MethodPost && r.URL.Path == "/v1/gardens/3/plants":
|
|
w.WriteHeader(http.StatusCreated)
|
|
_, _ = w.Write([]byte(`{"plant":{"id":5,"garden_id":3,"name":"Tomate"}}`))
|
|
case r.Method == http.MethodPost && r.URL.Path == "/v1/gardens/3/tasks":
|
|
body, _ := io.ReadAll(r.Body)
|
|
if err := json.Unmarshal(body, &taskInput); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
_, _ = w.Write([]byte(`{"task":{"id":12,"garden_id":3,"title":"Anbinden"}}`))
|
|
case r.Method == http.MethodGet && r.URL.Path == "/v1/gardens/3/species/7/task-templates":
|
|
_, _ = w.Write([]byte(`{"task_templates":[{"id":9,"species_id":7,"title":"Ausgeizen"}]}`))
|
|
case r.Method == http.MethodPost && r.URL.Path == "/v1/gardens/3/plants/5/task-template-opt-outs/9":
|
|
optedOut = true
|
|
w.WriteHeader(http.StatusNoContent)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
app := newAPIBackedTestApplication(t, apiHandler)
|
|
form := url.Values{
|
|
"name": {"Tomate"}, "species_id": {"7"}, "status": {"active"},
|
|
"plant_task_id": {"0"}, "plant_task_title": {"Anbinden"}, "plant_task_description": {"Locker befestigen"},
|
|
"plant_task_due_at_start": {""}, "plant_task_due_at_end": {""}, "plant_task_priority": {"3"}, "plant_task_active": {"true"}, "plant_task_delete": {"false"},
|
|
"plant_template_active_9": {"false"},
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, "/g/3/plants/new", 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.plantCreatePost(response, request)
|
|
if response.Code != http.StatusSeeOther || taskInput.Title == nil || *taskInput.Title != "Anbinden" || taskInput.PlantID == nil || *taskInput.PlantID != 5 || taskInput.Active == nil || !*taskInput.Active || !optedOut {
|
|
t.Fatalf("status=%d task=%+v optedOut=%v body=%s", response.Code, taskInput, optedOut, response.Body.String())
|
|
}
|
|
}
|