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

151 lines
5.6 KiB
Go

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)
}
}