package api import ( "net/http" "testing" "gardomatic.kleiax.de/internal/storage" ) type locationTestModel struct { items map[int]storage.Location nextID int } func (m *locationTestModel) Insert(value storage.Location) (storage.Location, error) { m.nextID++ value.ID = m.nextID value.Version = 1 m.items[value.ID] = value return value, nil } func (m *locationTestModel) Get(gardenID, id int) (storage.Location, error) { value, ok := m.items[id] if !ok || value.GardenID != gardenID { return storage.Location{}, storage.ErrRecordNotFound } return value, nil } func (m *locationTestModel) GetAllForGarden(gardenID int) ([]storage.Location, error) { result := []storage.Location{} for _, value := range m.items { if value.GardenID == gardenID { result = append(result, value) } } return result, nil } func (m *locationTestModel) Update(gardenID int, value storage.Location) (storage.Location, error) { if _, err := m.Get(gardenID, value.ID); err != nil { return storage.Location{}, err } value.Version++ m.items[value.ID] = value return value, nil } func (m *locationTestModel) Delete(gardenID, id int) error { if _, err := m.Get(gardenID, id); err != nil { return err } delete(m.items, id) return nil } type plantLocationTestModel struct { items map[int]storage.PlantLocation nextID int } func (m *plantLocationTestModel) Insert(_ int, value storage.PlantLocation) (storage.PlantLocation, error) { m.nextID++ value.ID = m.nextID value.Version = 1 m.items[value.ID] = value return value, nil } func (m *plantLocationTestModel) Get(_ int, id int) (storage.PlantLocation, error) { value, ok := m.items[id] if !ok { return storage.PlantLocation{}, storage.ErrRecordNotFound } return value, nil } func (m *plantLocationTestModel) GetAllForPlant(_ int, plantID int) ([]storage.PlantLocation, error) { result := []storage.PlantLocation{} for _, value := range m.items { if value.PlantID == plantID { result = append(result, value) } } return result, nil } func (m *plantLocationTestModel) GetAllForLocation(_ int, locationID int) ([]storage.PlantLocation, error) { result := []storage.PlantLocation{} for _, value := range m.items { if value.LocationID == locationID { result = append(result, value) } } return result, nil } func (m *plantLocationTestModel) Update(_ int, value storage.PlantLocation) (storage.PlantLocation, error) { value.Version++ m.items[value.ID] = value return value, nil } func (m *plantLocationTestModel) Delete(_ int, id int) error { if _, ok := m.items[id]; !ok { return storage.ErrRecordNotFound } delete(m.items, id) return nil } func TestLocationsRejectForeignParentsAndCycles(t *testing.T) { app, _, members := newGardenTestApplication() user := storage.User{ID: 7, Activated: true} members.members[[2]int{3, user.ID}] = storage.GardenMember{GardenID: 3, UserID: user.ID, Role: storage.GardenRoleMember} rootID := 1 locations := &locationTestModel{items: map[int]storage.Location{1: {ID: 1, GardenID: 3, Name: "Beet", CreatedBy: user.ID, Version: 1}, 2: {ID: 2, GardenID: 3, ParentID: &rootID, Name: "Reihe", CreatedBy: user.ID, Version: 1}, 9: {ID: 9, GardenID: 4, Name: "Fremd", Version: 1}}, nextID: 9} app.models.Locations = locations foreign := serveResourceRequest(app, user, http.MethodPost, "/v1/gardens/3/locations", []byte(`{"name":"Topf","parent_id":9}`)) if foreign.Code != http.StatusUnprocessableEntity { t.Fatalf("foreign parent: got %d, want %d; %s", foreign.Code, http.StatusUnprocessableEntity, foreign.Body.String()) } cycle := serveResourceRequest(app, user, http.MethodPatch, "/v1/gardens/3/locations/1", []byte(`{"parent_id":2}`)) if cycle.Code != http.StatusUnprocessableEntity { t.Fatalf("cycle: got %d, want %d; %s", cycle.Code, http.StatusUnprocessableEntity, cycle.Body.String()) } foreignRead := serveResourceRequest(app, user, http.MethodGet, "/v1/gardens/3/locations/9", nil) if foreignRead.Code != http.StatusNotFound { t.Fatalf("foreign read: got %d, want %d", foreignRead.Code, http.StatusNotFound) } cleared := serveResourceRequest(app, user, http.MethodPatch, "/v1/gardens/3/locations/2", []byte(`{"clear_parent_id":true}`)) if cleared.Code != http.StatusOK || locations.items[2].ParentID != nil { t.Fatalf("clear parent: status=%d body=%s", cleared.Code, cleared.Body.String()) } } func TestPlantLocationIsScopedToGarden(t *testing.T) { app, _, members := newGardenTestApplication() user := storage.User{ID: 8, Activated: true} members.members[[2]int{3, user.ID}] = storage.GardenMember{GardenID: 3, UserID: user.ID, Role: storage.GardenRoleMember} app.models.Plants = &plantTestModel{items: map[int]storage.Plant{5: {ID: 5, GardenID: 3, Name: "Tomate", Status: "active"}}} app.models.Locations = &locationTestModel{items: map[int]storage.Location{6: {ID: 6, GardenID: 3, Name: "Beet"}, 9: {ID: 9, GardenID: 4, Name: "Fremd"}}} assignments := &plantLocationTestModel{items: map[int]storage.PlantLocation{}, nextID: 10} app.models.PlantLocations = assignments foreign := serveResourceRequest(app, user, http.MethodPost, "/v1/gardens/3/plants/5/locations", []byte(`{"location_id":9,"quantity":1}`)) if foreign.Code != http.StatusUnprocessableEntity { t.Fatalf("foreign assignment: got %d, want %d; %s", foreign.Code, http.StatusUnprocessableEntity, foreign.Body.String()) } created := serveResourceRequest(app, user, http.MethodPost, "/v1/gardens/3/plants/5/locations", []byte(`{"location_id":6,"quantity":3}`)) if created.Code != http.StatusCreated { t.Fatalf("assignment: got %d, want %d; %s", created.Code, http.StatusCreated, created.Body.String()) } if got := assignments.items[11].Quantity; got != 3 { t.Errorf("quantity: got %d, want 3", got) } }