Files
Gardomatic/internal/api/tasks_test.go
T
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

134 lines
5.8 KiB
Go

package api
import (
"net/http"
"testing"
"time"
"gardomatic.kleiax.de/internal/storage"
)
type taskTestModel struct {
items map[int]storage.Task
nextID int
}
func (m *taskTestModel) Insert(value storage.Task) (storage.Task, error) {
if value.RepeatFromID != nil {
for _, existing := range m.items {
if existing.RepeatFromID != nil && *existing.RepeatFromID == *value.RepeatFromID {
return storage.Task{}, storage.ErrConflict
}
}
}
if value.TemplateID != nil && value.PlantID != nil && value.GeneratedFor != nil {
for _, existing := range m.items {
if existing.TemplateID != nil && *existing.TemplateID == *value.TemplateID && existing.PlantID != nil && *existing.PlantID == *value.PlantID && existing.GeneratedFor != nil && existing.GeneratedFor.Format("2006-01-02") == value.GeneratedFor.Format("2006-01-02") {
return storage.Task{}, storage.ErrConflict
}
}
}
m.nextID++
value.ID, value.Version = m.nextID, 1
m.items[value.ID] = value
return value, nil
}
func TestCompletingRecurringTaskCreatesNextCalendarOccurrence(t *testing.T) {
app, _, members := newGardenTestApplication()
user := storage.User{ID: 12, Activated: true}
members.members[[2]int{3, user.ID}] = storage.GardenMember{GardenID: 3, UserID: user.ID, Role: storage.GardenRoleMember}
dueStart := time.Date(2026, time.January, 31, 8, 0, 0, 0, time.UTC)
dueEnd := time.Date(2026, time.January, 31, 18, 0, 0, 0, time.UTC)
app.models.Tasks = &taskTestModel{items: map[int]storage.Task{101: {
ID: 101, GardenID: 3, Title: "Düngen", DueAtStart: &dueStart, DueAtEnd: &dueEnd,
Recurrence: storage.TaskRecurrenceMonthly, RecurrenceInterval: 2, Active: true, CreatedBy: user.ID, Version: 1,
}}, nextID: 101}
completed := serveResourceRequest(app, user, http.MethodPatch, "/v1/gardens/3/tasks/101", []byte(`{"completed":true}`))
if completed.Code != http.StatusOK {
t.Fatalf("complete: got %d; %s", completed.Code, completed.Body.String())
}
next := app.models.Tasks.(*taskTestModel).items[102]
if next.RepeatFromID == nil || *next.RepeatFromID != 101 || next.CompletedAt != nil {
t.Fatalf("next occurrence links: %+v", next)
}
if got := next.DueAtStart.Format(time.RFC3339); got != "2026-03-31T08:00:00Z" {
t.Errorf("next start = %s", got)
}
if got := next.DueAtEnd.Format(time.RFC3339); got != "2026-03-31T18:00:00Z" {
t.Errorf("next end = %s", got)
}
completedAgain := serveResourceRequest(app, user, http.MethodPatch, "/v1/gardens/3/tasks/101", []byte(`{"completed":true}`))
if completedAgain.Code != http.StatusOK || len(app.models.Tasks.(*taskTestModel).items) != 2 {
t.Fatalf("repeated completion created a duplicate: status=%d tasks=%+v", completedAgain.Code, app.models.Tasks.(*taskTestModel).items)
}
}
func (m *taskTestModel) Get(gardenID, id int) (storage.Task, error) {
value, ok := m.items[id]
if !ok || value.GardenID != gardenID {
return storage.Task{}, storage.ErrRecordNotFound
}
return value, nil
}
func (m *taskTestModel) GetAllForGarden(gardenID int) ([]storage.Task, error) {
result := []storage.Task{}
for _, value := range m.items {
if value.GardenID == gardenID {
result = append(result, value)
}
}
return result, nil
}
func (m *taskTestModel) Update(gardenID int, value storage.Task) (storage.Task, error) {
if _, err := m.Get(gardenID, value.ID); err != nil {
return storage.Task{}, err
}
value.Version++
m.items[value.ID] = value
return value, nil
}
func (m *taskTestModel) Delete(gardenID, id int) error {
if _, err := m.Get(gardenID, id); err != nil {
return err
}
delete(m.items, id)
return nil
}
func TestTasksAreScopedAndValidateReferences(t *testing.T) {
app, _, members := newGardenTestApplication()
user := storage.User{ID: 12, Activated: true}
members.members[[2]int{3, user.ID}] = storage.GardenMember{GardenID: 3, UserID: user.ID, Role: storage.GardenRoleMember}
app.models.Tasks = &taskTestModel{items: map[int]storage.Task{90: {ID: 90, GardenID: 4, Title: "Fremd", Version: 1}}, nextID: 100}
app.models.Plants = &plantTestModel{items: map[int]storage.Plant{5: {ID: 5, GardenID: 3, Name: "Tomate"}, 9: {ID: 9, GardenID: 4, Name: "Fremd"}}}
app.models.Locations = &locationTestModel{items: map[int]storage.Location{6: {ID: 6, GardenID: 3, Name: "Beet"}, 8: {ID: 8, GardenID: 4, Name: "Fremd"}}}
foreignReference := serveResourceRequest(app, user, http.MethodPost, "/v1/gardens/3/tasks", []byte(`{"title":"Gießen","plant_id":9,"location_id":8}`))
if foreignReference.Code != http.StatusUnprocessableEntity {
t.Fatalf("foreign references: got %d; %s", foreignReference.Code, foreignReference.Body.String())
}
created := serveResourceRequest(app, user, http.MethodPost, "/v1/gardens/3/tasks", []byte(`{"title":"Gießen","plant_id":5,"location_id":6,"priority":5}`))
if created.Code != http.StatusCreated {
t.Fatalf("create: got %d; %s", created.Code, created.Body.String())
}
value := app.models.Tasks.(*taskTestModel).items[101]
if value.CreatedBy != user.ID || value.GardenID != 3 {
t.Errorf("created task scope: %+v", value)
}
completed := serveResourceRequest(app, user, http.MethodPatch, "/v1/gardens/3/tasks/101", []byte(`{"completed":true}`))
if completed.Code != http.StatusOK || app.models.Tasks.(*taskTestModel).items[101].CompletedAt == nil {
t.Fatalf("complete: got %d; %s", completed.Code, completed.Body.String())
}
cleared := serveResourceRequest(app, user, http.MethodPatch, "/v1/gardens/3/tasks/101", []byte(`{"clear_plant_id":true,"clear_location_id":true}`))
if cleared.Code != http.StatusOK || app.models.Tasks.(*taskTestModel).items[101].PlantID != nil || app.models.Tasks.(*taskTestModel).items[101].LocationID != nil {
t.Fatalf("clear references: got %d; %s", cleared.Code, cleared.Body.String())
}
foreignRead := serveResourceRequest(app, user, http.MethodGet, "/v1/gardens/3/tasks/90", nil)
if foreignRead.Code != http.StatusNotFound {
t.Fatalf("foreign read: got %d", foreignRead.Code)
}
}