package web import ( "errors" "net/http" "net/http/httptest" "strings" "testing" "time" "gardomatic.kleiax.de/lib/client" ) func TestHumanDate(t *testing.T) { tests := []struct { name string tm time.Time want string }{ { name: "UTC", tm: time.Date(2022, 3, 17, 10, 15, 0, 0, time.UTC), want: "17 Mar 2022 at 10:15", }, { name: "Empty", tm: time.Time{}, want: "", }, { name: "CET", tm: time.Date(2022, 3, 17, 10, 15, 0, 0, time.FixedZone("CET", 1*60*60)), want: "17 Mar 2022 at 09:15", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { hd := humanDate(tt.tm) if hd != tt.want { t.Errorf("humanDate(): got %q, want %q", hd, tt.want) } }) } } func TestGlobalSpeciesControlsArePermissionAware(t *testing.T) { app := newTestApplication(t) garden := client.Garden{ID: 3, Name: "Hinterhof", Role: "viewer"} for _, test := range []struct { name string user client.User wantGlobal bool wantAdmin bool }{ {"admin", client.User{ID: 1, Activated: true, Role: "application:admin", Permissions: []string{"global_species:write", "roles:manage"}}, true, true}, {"user", client.User{ID: 2, Activated: true, Role: "application:user"}, false, false}, } { t.Run(test.name, func(t *testing.T) { data := &templateData{commonTemplateData: commonTemplateData{IsAuthenticated: true, IsActivated: true, CurrentUser: &test.user, Garden: &garden, Form: speciesForm{Errors: map[string]string{}}}} response := httptest.NewRecorder() app.render(response, http.StatusOK, "species_form.tmpl", data) body := response.Body.String() if strings.Contains(body, "name='global'") != test.wantGlobal { t.Errorf("global checkbox visibility mismatch: %s", body) } if strings.Contains(body, "href='/admin") != test.wantAdmin { t.Errorf("admin menu visibility mismatch: %s", body) } }) } } func TestSpeciesCardsAndReadOnlyGlobalDetails(t *testing.T) { app := newTestApplication(t) user := client.User{ID: 2, Activated: true, Role: "application:user"} garden := client.Garden{ID: 3, Name: "Hinterhof", Role: "viewer"} species := client.Species{ID: 7, CommonName: "Tomate", Cultivar: "Roma"} listData := &templateData{commonTemplateData: commonTemplateData{IsAuthenticated: true, IsActivated: true, CurrentUser: &user, Garden: &garden}, speciesTemplateData: speciesTemplateData{Species: []client.Species{species}}} listResponse := httptest.NewRecorder() app.render(listResponse, http.StatusOK, "species.tmpl", listData) listBody := listResponse.Body.String() if !strings.Contains(listBody, "data-card-href='/g/3/species/edit/7'") || strings.Contains(listBody, ">Bearbeiten") { t.Fatalf("species card is not exclusively clickable: %s", listBody) } generalData := &templateData{commonTemplateData: commonTemplateData{IsAuthenticated: true, IsActivated: true, CurrentUser: &user, Garden: &garden, Form: speciesForm{Global: true, CommonName: "Tomate", Errors: map[string]string{}}}, speciesTemplateData: speciesTemplateData{SpeciesID: 7, WizardStep: "general", TaskTemplates: map[int][]client.SpeciesTaskTemplate{7: {{ID: 9, SpeciesID: 7, Title: "Ausgeizen"}}}}} generalResponse := httptest.NewRecorder() app.render(generalResponse, http.StatusOK, "species_form.tmpl", generalData) if body := generalResponse.Body.String(); !strings.Contains(body, "Diese Art ist nur lesbar") || !strings.Contains(body, "disabled") || strings.Contains(body, "Art löschen") { t.Fatalf("global species general details are not read-only: %s", body) } tasksData := &templateData{commonTemplateData: generalData.commonTemplateData, speciesTemplateData: speciesTemplateData{SpeciesID: 7, WizardStep: "tasks", TaskTemplates: map[int][]client.SpeciesTaskTemplate{7: {{ID: 9, SpeciesID: 7, Title: "Ausgeizen"}}}}} tasksResponse := httptest.NewRecorder() app.render(tasksResponse, http.StatusOK, "species_form.tmpl", tasksData) if body := tasksResponse.Body.String(); !strings.Contains(body, "Ausgeizen") || !strings.Contains(body, "Bearbeitungsbereiche") || strings.Contains(body, "Aufgabenvorlage hinzufügen") { t.Fatalf("global species task templates are not read-only: %s", body) } } func TestEditableSpeciesDetailOffersDelete(t *testing.T) { app := newTestApplication(t) user := client.User{ID: 1, Activated: true, Role: "application:admin", Permissions: []string{"global_species:write", "roles:manage"}} garden := client.Garden{ID: 3, Name: "Hinterhof", Role: "viewer"} data := &templateData{commonTemplateData: commonTemplateData{IsAuthenticated: true, IsActivated: true, CurrentUser: &user, Garden: &garden, Form: speciesForm{Global: true, CommonName: "Tomate", Errors: map[string]string{}}}, speciesTemplateData: speciesTemplateData{SpeciesID: 7, TaskTemplates: map[int][]client.SpeciesTaskTemplate{}}} response := httptest.NewRecorder() app.render(response, http.StatusOK, "species_form.tmpl", data) if body := response.Body.String(); !strings.Contains(body, "formaction='/g/3/species/delete/7'") || !strings.Contains(body, "Art löschen") || strings.Contains(body, "class='delete-form'") { t.Fatalf("delete action missing from editable species: %s", body) } } func TestGardenContextIsKeptInAccountAndSettingsLinks(t *testing.T) { app := newTestApplication(t) user := client.User{ID: 1, Name: "Alice", Activated: true} garden := client.Garden{ID: 3, Name: "Hinterhof", Role: "owner"} data := &templateData{commonTemplateData: commonTemplateData{IsAuthenticated: true, IsActivated: true, CurrentUser: &user, Garden: &garden}} response := httptest.NewRecorder() app.render(response, http.StatusOK, "settings.tmpl", data) body := response.Body.String() for _, want := range []string{"href='/account?garden=3'", "href='/settings?garden=3'", "href='/g/3'>Hinterhof", "name='journalEditor'", "Einfaches Textfeld"} { if !strings.Contains(body, want) { t.Errorf("garden-aware navigation is missing %q: %s", want, body) } } } func TestProtectedDemoAccountHidesAccountMutationForms(t *testing.T) { app := newTestApplication(t) user := client.User{ID: 1, Name: "Demo", Email: "demo@example.com", Activated: true} data := &templateData{ commonTemplateData: commonTemplateData{IsAuthenticated: true, IsActivated: true, CurrentUser: &user, Form: accountForm{Errors: map[string]string{}}}, accountTemplateData: accountTemplateData{AccountProtected: true}, } response := httptest.NewRecorder() app.render(response, http.StatusOK, "account.tmpl", data) body := response.Body.String() if !strings.Contains(body, "Gemeinsames Demokonto") { t.Fatalf("protected account notice is missing: %s", body) } for _, forbidden := range []string{"action='/account/profile'", "action='/account/password'", "action='/account/email'", "Widerrufen"} { if strings.Contains(body, forbidden) { t.Errorf("protected account still offers %q: %s", forbidden, body) } } } func TestAdminTemplateKeepsSelectedGardenInNavigationAndForms(t *testing.T) { app := newTestApplication(t) user := client.User{ID: 1, Name: "Alice", Activated: true, Permissions: []string{"roles:manage"}} garden := client.Garden{ID: 3, Name: "Hinterhof", Role: "owner"} editor := globalRoleEditor("roles-settings", "Instanzrollen", "Instanz", "application", nil, applicationPermissionOptions(), "token") editor.Garden = &garden data := &templateData{ commonTemplateData: commonTemplateData{IsAuthenticated: true, IsActivated: true, CurrentUser: &user, Garden: &garden}, adminTemplateData: adminTemplateData{ApplicationSettings: &client.ApplicationSettings{}, RoleEditors: []roleEditorData{editor}}, } response := httptest.NewRecorder() app.render(response, http.StatusOK, "admin.tmpl", data) body := response.Body.String() for _, want := range []string{"href='/admin?garden=3'", "action='/admin/application-settings?garden=3'", "action='/admin/user-invite?garden=3'", "action='/admin/roles/new?garden=3'"} { if !strings.Contains(body, want) { t.Errorf("admin page does not retain garden context in %q: %s", want, body) } } } func TestFooterAndPublicInformationPages(t *testing.T) { app := newTestApplication(t) health := client.Health{Status: "available", ServerTime: time.Date(2026, 9, 11, 10, 0, 0, 0, time.UTC), SystemInfo: client.SystemInfo{Environment: "production", Version: "v1.2.3"}} data := &templateData{commonTemplateData: commonTemplateData{CurrentYear: 2026, Health: &health, SystemTime: time.Date(2026, 9, 11, 12, 0, 0, 0, time.Local), WebVersion: "v1.2.3"}} response := httptest.NewRecorder() app.render(response, http.StatusOK, "healthcheck.tmpl", data) body := response.Body.String() for _, want := range []string{"Systemstatus", "Serverzeit", "Systemzeit", "production", "v1.2.3", "https://git.kleiax.de/kleiax/Gardomatic", "https://kleiax.de", "href='/datenschutz'"} { if !strings.Contains(body, want) { t.Errorf("health page or footer is missing %q: %s", want, body) } } response = httptest.NewRecorder() app.render(response, http.StatusOK, "privacy.tmpl", &templateData{}) if body = response.Body.String(); !strings.Contains(body, "Verarbeitete Daten") || !strings.Contains(body, "Verantwortlicher") { t.Fatalf("privacy page is incomplete: %s", body) } } func TestGardenCreationControlRequiresPermission(t *testing.T) { app := newTestApplication(t) for _, test := range []struct { name string user client.User want bool }{ {"allowed", client.User{Permissions: []string{"gardens:create"}}, true}, {"denied", client.User{Permissions: []string{}}, false}, } { t.Run(test.name, func(t *testing.T) { response := httptest.NewRecorder() app.render(response, http.StatusOK, "gardens.tmpl", &templateData{commonTemplateData: commonTemplateData{CurrentUser: &test.user, Filters: map[string]string{}}}) if got := strings.Contains(response.Body.String(), "href='/gardens/new'"); got != test.want { t.Fatalf("garden creation link visibility = %v, want %v", got, test.want) } }) } } func TestJournalFormOffersNativeCaptureAndCameraSwitching(t *testing.T) { app := newTestApplication(t) garden := client.Garden{ID: 3, Name: "Hinterhof", Role: "owner"} data := &templateData{commonTemplateData: commonTemplateData{Garden: &garden, Form: journalForm{Errors: map[string]string{}}}} response := httptest.NewRecorder() app.render(response, http.StatusOK, "journal_form.tmpl", data) body := response.Body.String() for _, want := range []string{"Foto mit Gerät aufnehmen", "data-journal-generated-files", "data-camera-switch", "data-video-switch-camera"} { if !strings.Contains(body, want) { t.Errorf("mobile journal form is missing %q: %s", want, body) } } if strings.Contains(body, "capture='environment'") { t.Errorf("journal form still contains the redundant native camera picker: %s", body) } } func TestServerErrorRendersFriendlyPage(t *testing.T) { app := newTestApplication(t) response := httptest.NewRecorder() app.serverError(response, errors.New("database unavailable")) if response.Code != http.StatusInternalServerError { t.Fatalf("status: got %d, want %d", response.Code, http.StatusInternalServerError) } body := response.Body.String() if !strings.Contains(body, "Etwas ist schiefgelaufen") || strings.Contains(body, "database unavailable") { t.Fatalf("friendly error page missing or leaks internal details: %s", body) } } func TestSpeciesFormUsesCategoryDropdown(t *testing.T) { app := newTestApplication(t) user := client.User{ID: 1, Activated: true} garden := client.Garden{ID: 3, Name: "Hinterhof", Role: "owner"} data := &templateData{commonTemplateData: commonTemplateData{IsAuthenticated: true, IsActivated: true, CurrentUser: &user, Garden: &garden, Form: speciesForm{CategoryID: 2, Errors: map[string]string{}}}, adminTemplateData: adminTemplateData{SpeciesCategories: []client.SpeciesCategory{{ID: 1, Name: "Gemüse", Active: true}, {ID: 2, Name: "Alt", Active: false}}}} response := httptest.NewRecorder() app.render(response, http.StatusOK, "species_form.tmpl", data) body := response.Body.String() if !strings.Contains(body, "
", "href='/g/3/tasks/calendar'>Kalender", "href='/gardens/edit/3'>Bearbeiten"} { if !strings.Contains(dashboardBody, want) { t.Errorf("garden dashboard is missing %q: %s", want, dashboardBody) } } viewerGarden := garden viewerGarden.Role = "viewer" viewerResponse := httptest.NewRecorder() app.render(viewerResponse, http.StatusOK, "dashboard.tmpl", &templateData{commonTemplateData: commonTemplateData{Garden: &viewerGarden}}) if strings.Contains(viewerResponse.Body.String(), "/gardens/edit/3") { t.Fatalf("read-only garden dashboard contains edit link: %s", viewerResponse.Body.String()) } plantsResponse := httptest.NewRecorder() app.render(plantsResponse, http.StatusOK, "plant.tmpl", &templateData{commonTemplateData: commonTemplateData{IsAuthenticated: true, IsActivated: true, CurrentUser: &user, Garden: &garden}}) plantsBody := plantsResponse.Body.String() if strings.Count(plantsBody, garden.Name) != 1 || strings.Contains(plantsBody, garden.Description) { t.Fatalf("garden details should only appear once in the header away from the dashboard: %s", plantsBody) } gardensResponse := httptest.NewRecorder() app.render(gardensResponse, http.StatusOK, "gardens.tmpl", &templateData{commonTemplateData: commonTemplateData{IsAuthenticated: true, IsActivated: true, CurrentUser: &user}, gardenTemplateData: gardenTemplateData{Gardens: []client.Garden{garden}}}) if gardensBody := gardensResponse.Body.String(); strings.Contains(gardensBody, "/gardens/edit/3") { t.Fatalf("garden list still contains edit link: %s", gardensBody) } } func TestEmptyOptionalSpeciesDimensionsDoNotBlockSubmit(t *testing.T) { app := newTestApplication(t) user := client.User{ID: 1, Activated: true} garden := client.Garden{ID: 3, Name: "Hinterhof", Role: "owner"} data := &templateData{commonTemplateData: commonTemplateData{IsAuthenticated: true, IsActivated: true, CurrentUser: &user, Garden: &garden, Form: speciesForm{Errors: map[string]string{}}}} response := httptest.NewRecorder() app.render(response, http.StatusOK, "species_form.tmpl", data) body := response.Body.String() for _, field := range []string{"spacing_cm", "height_cm_from", "height_cm_to", "width_cm_from", "width_cm_to"} { fieldStart := strings.Index(body, "name='"+field+"'") fieldEnd := -1 if fieldStart >= 0 { fieldEnd = strings.Index(body[fieldStart:], ">") } if fieldStart < 0 || fieldEnd < 0 || !strings.Contains(body[fieldStart:fieldStart+fieldEnd], "value=''") { t.Errorf("optional field %s should render empty: %s", field, body) } } if got := strings.Count(body, "class='dimension-range'"); got != 2 { t.Errorf("dimension ranges: got %d composite controls, want 2: %s", got, body) } } func TestTaskDueFormatsWindows(t *testing.T) { start := time.Date(2026, 9, 2, 8, 0, 0, 0, time.Local) end := time.Date(2026, 9, 3, 18, 0, 0, 0, time.Local) if got := taskDue(client.Task{DueAtStart: &start, DueAtEnd: &end}); got != "02.09.2026 08:00 - 03.09.2026 18:00" { t.Errorf("taskDue: %q", got) } if got := taskDue(client.Task{}); got != "Ohne Fälligkeit" { t.Errorf("taskDue empty: %q", got) } if got := taskDueDate(client.Task{DueAtStart: &start, DueAtEnd: &end}); got != "02.09.2026 - 03.09.2026" { t.Errorf("taskDueDate: %q", got) } } func TestDashboardTaskCardIsClickableAndDateOnly(t *testing.T) { app := newTestApplication(t) due := time.Date(2026, 9, 2, 8, 30, 0, 0, time.Local) garden := client.Garden{ID: 3, Name: "Hinterhof", Role: "owner"} data := &templateData{commonTemplateData: commonTemplateData{Garden: &garden}, taskTemplateData: taskTemplateData{Tasks: []client.Task{{ID: 7, GardenID: 3, Title: "Tomaten gießen", DueAtStart: &due}}}} response := httptest.NewRecorder() app.render(response, http.StatusOK, "dashboard.tmpl", data) body := response.Body.String() for _, want := range []string{"data-card-href='/g/3/tasks/edit/7'", ">ab 02.09.2026"} { if !strings.Contains(body, want) { t.Errorf("dashboard task is missing %q: %s", want, body) } } if strings.Contains(body, "08:30") || strings.Contains(body, ">Öffnen") { t.Fatalf("dashboard task still renders time or open link: %s", body) } } func TestTaskPaginationOnlyAppearsForMultiplePages(t *testing.T) { app := newTestApplication(t) garden := client.Garden{ID: 3, Name: "Hinterhof", Role: "owner"} base := &templateData{commonTemplateData: commonTemplateData{Garden: &garden, Filters: map[string]string{"status": "open"}}} response := httptest.NewRecorder() app.render(response, http.StatusOK, "tasks.tmpl", base) if strings.Contains(response.Body.String(), "class='pagination'") { t.Fatalf("single-page task list contains pagination: %s", response.Body.String()) } base.Pagination = &paginationData{Page: 1, TotalPages: 2, NextURL: "/g/3/tasks?page=2&status=open"} response = httptest.NewRecorder() app.render(response, http.StatusOK, "tasks.tmpl", base) body := response.Body.String() if !strings.Contains(body, "class='pagination'") || !strings.Contains(body, "Seite 1 von 2") { t.Fatalf("multi-page task list is missing pagination: %s", body) } } func TestSettingsOfferCollectionPageSizes(t *testing.T) { app := newTestApplication(t) response := httptest.NewRecorder() app.render(response, http.StatusOK, "settings.tmpl", &templateData{}) body := response.Body.String() for _, want := range []string{"name='entriesPerPage'", "Einträge pro Seite", "href='#list-settings'", "href='#view-settings'", "value='10'", "value='20'", "value='50'", "value='100'"} { if !strings.Contains(body, want) { t.Errorf("settings are missing %q: %s", want, body) } } } func TestAdminSettingsOfferSectionNavigation(t *testing.T) { app := newTestApplication(t) user := client.User{ID: 1, Role: "application:admin", Permissions: []string{"roles:manage"}} response := httptest.NewRecorder() applicationRoles := []client.Role{{Name: "application:admin", Scope: "application", Label: "Administrator", Permissions: []string{"roles:manage"}}} gardenRoles := []client.Role{{Name: "worker", Scope: "garden", Label: "Mitarbeiter", Permissions: []string{"garden:read"}}} applicationPermissions, gardenPermissions := applicationPermissionOptions(), gardenPermissionOptions() app.render(response, http.StatusOK, "admin.tmpl", &templateData{commonTemplateData: commonTemplateData{CurrentUser: &user}, adminTemplateData: adminTemplateData{AdminUsers: []client.User{{ID: 2, Name: "Ada", Email: "ada@example.com", Role: "application:user"}}, ApplicationSettings: &client.ApplicationSettings{}, ApplicationRoles: applicationRoles, GardenRoles: gardenRoles, ApplicationPermissions: applicationPermissions, GardenPermissions: gardenPermissions, EnvironmentVariables: []client.EnvironmentVariable{{Component: "API", Name: "GARDOMATIC_SMTP_PASSWORD", Value: "•••••••• (gesetzt)"}}, RoleEditors: []roleEditorData{globalRoleEditor("roles-settings", "Instanzrollen", "Instanz", "application", applicationRoles, applicationPermissions, ""), globalRoleEditor("garden-role-templates", "Gartenrollen", "Garten", "garden", gardenRoles, gardenPermissions, "")}}}) body := response.Body.String() for _, want := range []string{"class='settings-layout'", "href='#lifecycle-settings'", "href='#mail-settings'", "href='#environment-settings'", "href='#users-settings'", "href='#roles-settings'", "href='#species-categories'", "href='#task-priorities'", "id='users-settings'", "action='/admin/user-invite'", "NameNeu"} { if !strings.Contains(body, want) { t.Errorf("admin settings are missing %q: %s", want, body) } } } func TestInvitationActivationOffersInitialPasswordFields(t *testing.T) { app := newTestApplication(t) response := httptest.NewRecorder() app.render(response, http.StatusOK, "activate.tmpl", &templateData{commonTemplateData: commonTemplateData{Form: activationForm{Token: "invite-token", SetPassword: true, Errors: map[string]string{}}}}) body := response.Body.String() for _, want := range []string{"name='set_password'", "name='password'", "name='password_confirm'", "Passwort festlegen"} { if !strings.Contains(body, want) { t.Errorf("invitation activation is missing %q: %s", want, body) } } } func TestCollectionPagesUseSharedPagination(t *testing.T) { app := newTestApplication(t) garden := client.Garden{ID: 3, Name: "Hinterhof", Role: "owner"} user := client.User{ID: 1, Activated: true} for _, test := range []struct { name string data *templateData }{ {"gardens.tmpl", &templateData{}}, {"plant.tmpl", &templateData{commonTemplateData: commonTemplateData{Garden: &garden}, plantTemplateData: plantTemplateData{PlantLocations: map[int][]client.PlantLocation{}}, locationTemplateData: locationTemplateData{LocationNames: map[int]string{}}}}, {"species.tmpl", &templateData{commonTemplateData: commonTemplateData{Garden: &garden, CurrentUser: &user}}}, {"locations.tmpl", &templateData{commonTemplateData: commonTemplateData{Garden: &garden}}}, {"tasks.tmpl", &templateData{commonTemplateData: commonTemplateData{Garden: &garden, Filters: map[string]string{}}}}, {"journal.tmpl", &templateData{commonTemplateData: commonTemplateData{Garden: &garden}}}, {"images.tmpl", &templateData{commonTemplateData: commonTemplateData{Garden: &garden, Filters: map[string]string{}}}}, } { t.Run(test.name, func(t *testing.T) { test.data.Pagination = &paginationData{Page: 1, TotalPages: 2, NextURL: "/next?page=2"} response := httptest.NewRecorder() app.render(response, http.StatusOK, test.name, test.data) if body := response.Body.String(); !strings.Contains(body, "class='pagination'") || !strings.Contains(body, "href='/next?page=2'") { t.Fatalf("page does not render shared pagination: %s", body) } }) } } func TestTaskCardsAndEditDeleteAction(t *testing.T) { app := newTestApplication(t) user := client.User{ID: 2, Activated: true, Role: "application:user"} garden := client.Garden{ID: 3, Name: "Hinterhof", Role: "member"} task := client.Task{ID: 7, GardenID: 3, Title: "Tomaten gießen", CreatedBy: user.ID} listData := &templateData{commonTemplateData: commonTemplateData{IsAuthenticated: true, IsActivated: true, CurrentUser: &user, Garden: &garden, Filters: map[string]string{}}, taskTemplateData: taskTemplateData{Tasks: []client.Task{task}}} listResponse := httptest.NewRecorder() app.render(listResponse, http.StatusOK, "tasks.tmpl", listData) listBody := listResponse.Body.String() for _, want := range []string{"data-card-href='/g/3/tasks/edit/7'", "task-card-actions", ">Erledigt"} { if !strings.Contains(listBody, want) { t.Fatalf("task card is missing %q: %s", want, listBody) } } for _, unwanted := range []string{">Bearbeiten", "action='/g/3/tasks/delete/7'", ">Erledigen"} { if strings.Contains(listBody, unwanted) { t.Fatalf("task card still contains %q: %s", unwanted, listBody) } } formData := &templateData{commonTemplateData: commonTemplateData{IsAuthenticated: true, IsActivated: true, CurrentUser: &user, Garden: &garden, Form: taskForm{Title: task.Title, Errors: map[string]string{}}}, taskTemplateData: taskTemplateData{TaskID: 7, TaskCreatedBy: user.ID}} formResponse := httptest.NewRecorder() app.render(formResponse, http.StatusOK, "task_form.tmpl", formData) formBody := formResponse.Body.String() if !strings.Contains(formBody, "action='/g/3/tasks/delete/7'") || !strings.Contains(formBody, ">Aufgabe löschen") { t.Fatalf("delete action missing from task edit page: %s", formBody) } } func TestCustomGardenPermissionsControlResourceActions(t *testing.T) { app := newTestApplication(t) user := client.User{ID: 2, Activated: true} garden := client.Garden{ ID: 3, Name: "Hinterhof", Role: "garden:custom", Permissions: []string{"plants:create", "plants:update:own", "tasks:complete:other"}, } plantsResponse := httptest.NewRecorder() app.render(plantsResponse, http.StatusOK, "plant.tmpl", &templateData{ commonTemplateData: commonTemplateData{CurrentUser: &user, Garden: &garden, Filters: map[string]string{}}, plantTemplateData: plantTemplateData{ Plants: []client.Plant{{ID: 5, GardenID: 3, Name: "Eigene", CreatedBy: user.ID}, {ID: 6, GardenID: 3, Name: "Fremde", CreatedBy: 9}}, PlantLocations: map[int][]client.PlantLocation{}, }, locationTemplateData: locationTemplateData{LocationNames: map[int]string{}}, }) plantsBody := plantsResponse.Body.String() for _, want := range []string{"href='/g/3/plants/new'", "action='/g/3/plants/status/5'"} { if !strings.Contains(plantsBody, want) { t.Errorf("permitted plant action is missing %q", want) } } if strings.Contains(plantsBody, "action='/g/3/plants/status/6'") { t.Error("update action for another user's plant was shown") } tasksResponse := httptest.NewRecorder() app.render(tasksResponse, http.StatusOK, "tasks.tmpl", &templateData{ commonTemplateData: commonTemplateData{CurrentUser: &user, Garden: &garden, Filters: map[string]string{}}, taskTemplateData: taskTemplateData{Tasks: []client.Task{ {ID: 7, GardenID: 3, Title: "Eigene", CreatedBy: user.ID}, {ID: 8, GardenID: 3, Title: "Fremde", CreatedBy: 9}, }}, }) tasksBody := tasksResponse.Body.String() if strings.Contains(tasksBody, "href='/g/3/tasks/new'") || strings.Contains(tasksBody, "data-card-href='/g/3/tasks/edit/") { t.Error("task create or update action was shown without permission") } if strings.Contains(tasksBody, "action='/g/3/tasks/complete/7'") || !strings.Contains(tasksBody, "action='/g/3/tasks/complete/8'") { t.Error("own/other task completion permissions were not distinguished") } } func TestTaskTemplateFormAdaptsToTriggerType(t *testing.T) { app := newTestApplication(t) garden := client.Garden{ID: 3, Name: "Hinterhof", Role: "owner"} species := client.Species{ID: 7, CommonName: "Tomate"} priorities := []client.TaskPriority{{ID: 1, Name: "Dringend", Value: 8, Active: true}} data := &templateData{commonTemplateData: commonTemplateData{Garden: &garden, Form: taskTemplateForm{TriggerType: "month_of_year", DayFrom: 1, TriggerOffsetUnit: "day", DurationUnit: "week", RecurrenceInterval: 2, Priority: 8, Errors: map[string]string{}}}, adminTemplateData: adminTemplateData{TaskPriorities: priorities}, speciesTemplateData: speciesTemplateData{Species: []client.Species{species}, SpeciesID: 7}} response := httptest.NewRecorder() app.render(response, http.StatusOK, "task_template_form.tmpl", data) body := response.Body.String() for _, want := range []string{"", ">Datum", "", "name='day_from' value='1'", "", "", "data-trigger-fields='relative' hidden disabled", ">Wochen", ">Dringend", "Die nächste Aufgabe wird erst beim Erledigen angelegt."} { if !strings.Contains(body, want) { t.Errorf("task template form is missing %q: %s", want, body) } } for _, unwanted := range []string{"Jährlicher Zeitraum", ">Auslöser", "id='template-title' name='title' value='' required", "Startdatum", "Beginn relativ zum Typ", "Aufgabenzeitraum", "Nach letzter Erledigung"} { if strings.Contains(body, unwanted) { t.Errorf("task template form still contains %q: %s", unwanted, body) } } if strings.Index(body, "