134 lines
5.8 KiB
Go
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)
|
|
}
|
|
}
|