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

251 lines
12 KiB
Go

package api
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"gardomatic.kleiax.de/internal/storage"
"github.com/julienschmidt/httprouter"
)
type speciesTestModel struct {
items map[int]storage.Species
nextID int
lastGardenID int
}
func (m *speciesTestModel) Insert(species storage.Species) (storage.Species, error) {
m.nextID++
species.ID = m.nextID
species.Version = 1
m.items[species.ID] = species
return species, nil
}
func (m *speciesTestModel) Get(gardenID, id int) (storage.Species, error) {
m.lastGardenID = gardenID
species, ok := m.items[id]
if !ok || (species.GardenID != nil && *species.GardenID != gardenID) {
return storage.Species{}, storage.ErrRecordNotFound
}
return species, nil
}
func (m *speciesTestModel) GetAllForGarden(gardenID int) ([]storage.Species, error) {
m.lastGardenID = gardenID
result := []storage.Species{}
for _, species := range m.items {
if species.GardenID == nil || *species.GardenID == gardenID {
result = append(result, species)
}
}
return result, nil
}
func (m *speciesTestModel) Update(gardenID int, species storage.Species) (storage.Species, error) {
m.lastGardenID = gardenID
species.Version++
m.items[species.ID] = species
return species, nil
}
func (m *speciesTestModel) Delete(gardenID, id int) error {
species, ok := m.items[id]
if !ok || species.GardenID == nil && gardenID != 0 || species.GardenID != nil && *species.GardenID != gardenID {
return storage.ErrRecordNotFound
}
m.lastGardenID = gardenID
delete(m.items, id)
return nil
}
type plantTestModel struct {
items map[int]storage.Plant
nextID int
lastGardenID int
}
func (m *plantTestModel) Insert(plant storage.Plant) (storage.Plant, error) {
m.nextID++
plant.ID = m.nextID
plant.Version = 1
m.items[plant.ID] = plant
return plant, nil
}
func (m *plantTestModel) Get(gardenID, id int) (storage.Plant, error) {
m.lastGardenID = gardenID
plant, ok := m.items[id]
if !ok || plant.GardenID != gardenID {
return storage.Plant{}, storage.ErrRecordNotFound
}
return plant, nil
}
func (m *plantTestModel) GetAllForGarden(gardenID int) ([]storage.Plant, error) {
m.lastGardenID = gardenID
result := []storage.Plant{}
for _, plant := range m.items {
if plant.GardenID == gardenID {
result = append(result, plant)
}
}
return result, nil
}
func (m *plantTestModel) Update(gardenID int, plant storage.Plant) (storage.Plant, error) {
m.lastGardenID = gardenID
plant.Version++
m.items[plant.ID] = plant
return plant, nil
}
func (m *plantTestModel) Delete(gardenID, id int) error {
plant, ok := m.items[id]
if !ok || plant.GardenID != gardenID {
return storage.ErrRecordNotFound
}
delete(m.items, id)
return nil
}
func serveResourceRequest(app *application, user storage.User, method, path string, body []byte) *httptest.ResponseRecorder {
router := httprouter.New()
protect := func(handler http.HandlerFunc) http.HandlerFunc {
return app.requireActivatedUser(app.requireGardenMember(handler))
}
router.HandlerFunc(http.MethodPost, "/v1/gardens/:gardenID/species", protect(app.createSpeciesHandler))
router.HandlerFunc(http.MethodGet, "/v1/gardens/:gardenID/species", protect(app.listSpeciesHandler))
router.HandlerFunc(http.MethodGet, "/v1/gardens/:gardenID/species/:id", protect(app.showSpeciesHandler))
router.HandlerFunc(http.MethodPatch, "/v1/gardens/:gardenID/species/:id", protect(app.updateSpeciesHandler))
router.HandlerFunc(http.MethodDelete, "/v1/gardens/:gardenID/species/:id", protect(app.deleteSpeciesHandler))
router.HandlerFunc(http.MethodPost, "/v1/gardens/:gardenID/species/:id/task-templates", protect(app.createSpeciesTaskTemplateHandler))
router.HandlerFunc(http.MethodGet, "/v1/gardens/:gardenID/species/:id/task-templates", protect(app.listSpeciesTaskTemplatesHandler))
router.HandlerFunc(http.MethodGet, "/v1/gardens/:gardenID/species/:id/task-templates/:templateID", protect(app.showSpeciesTaskTemplateHandler))
router.HandlerFunc(http.MethodPatch, "/v1/gardens/:gardenID/species/:id/task-templates/:templateID", protect(app.updateSpeciesTaskTemplateHandler))
router.HandlerFunc(http.MethodDelete, "/v1/gardens/:gardenID/species/:id/task-templates/:templateID", protect(app.deleteSpeciesTaskTemplateHandler))
router.HandlerFunc(http.MethodPost, "/v1/gardens/:gardenID/plants", protect(app.createPlantHandler))
router.HandlerFunc(http.MethodGet, "/v1/gardens/:gardenID/plants", protect(app.listPlantsHandler))
router.HandlerFunc(http.MethodGet, "/v1/gardens/:gardenID/plants/:id", protect(app.showPlantHandler))
router.HandlerFunc(http.MethodPatch, "/v1/gardens/:gardenID/plants/:id", protect(app.updatePlantHandler))
router.HandlerFunc(http.MethodDelete, "/v1/gardens/:gardenID/plants/:id", protect(app.deletePlantHandler))
router.HandlerFunc(http.MethodPost, "/v1/gardens/:gardenID/locations", protect(app.createLocationHandler))
router.HandlerFunc(http.MethodGet, "/v1/gardens/:gardenID/locations", protect(app.listLocationsHandler))
router.HandlerFunc(http.MethodGet, "/v1/gardens/:gardenID/locations/:id", protect(app.showLocationHandler))
router.HandlerFunc(http.MethodPatch, "/v1/gardens/:gardenID/locations/:id", protect(app.updateLocationHandler))
router.HandlerFunc(http.MethodDelete, "/v1/gardens/:gardenID/locations/:id", protect(app.deleteLocationHandler))
router.HandlerFunc(http.MethodPost, "/v1/gardens/:gardenID/tasks", protect(app.createTaskHandler))
router.HandlerFunc(http.MethodGet, "/v1/gardens/:gardenID/tasks", protect(app.listTasksHandler))
router.HandlerFunc(http.MethodGet, "/v1/gardens/:gardenID/tasks/:id", protect(app.showTaskHandler))
router.HandlerFunc(http.MethodPatch, "/v1/gardens/:gardenID/tasks/:id", protect(app.updateTaskHandler))
router.HandlerFunc(http.MethodDelete, "/v1/gardens/:gardenID/tasks/:id", protect(app.deleteTaskHandler))
router.HandlerFunc(http.MethodPost, "/v1/gardens/:gardenID/plants/:id/locations", protect(app.createPlantLocationHandler))
router.HandlerFunc(http.MethodGet, "/v1/gardens/:gardenID/plants/:id/locations", protect(app.listPlantLocationsHandler))
router.HandlerFunc(http.MethodPatch, "/v1/gardens/:gardenID/plants/:id/locations/:assignmentID", protect(app.updatePlantLocationHandler))
router.HandlerFunc(http.MethodDelete, "/v1/gardens/:gardenID/plants/:id/locations/:assignmentID", protect(app.deletePlantLocationHandler))
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
router.ServeHTTP(w, app.contextSetAuthenticatedUser(r, user))
})
request := httptest.NewRequest(method, path, bytes.NewReader(body))
request.Header.Set("Content-Type", "application/json")
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
return response
}
func TestSpeciesAndPlantsAreScopedToGarden(t *testing.T) {
app, _, members := newGardenTestApplication()
user := storage.User{ID: 5, Activated: true}
members.members[[2]int{3, user.ID}] = storage.GardenMember{GardenID: 3, UserID: user.ID, Role: storage.GardenRoleAdmin}
speciesModel := &speciesTestModel{items: make(map[int]storage.Species), nextID: 10}
plantsModel := &plantTestModel{items: make(map[int]storage.Plant), nextID: 20}
app.models.Species = speciesModel
app.models.Plants = plantsModel
speciesResponse := serveResourceRequest(app, user, http.MethodPost, "/v1/gardens/3/species", []byte(`{"common_name":"Tomate"}`))
if speciesResponse.Code != http.StatusCreated {
t.Fatalf("create species status: got %d, want %d; body: %s", speciesResponse.Code, http.StatusCreated, speciesResponse.Body.String())
}
createdSpecies := speciesModel.items[11]
if createdSpecies.GardenID == nil || *createdSpecies.GardenID != 3 {
t.Errorf("species garden: got %v, want 3", createdSpecies.GardenID)
}
plantResponse := serveResourceRequest(app, user, http.MethodPost, "/v1/gardens/3/plants", []byte(`{"species_id":11,"name":"Tomate am Zaun"}`))
if plantResponse.Code != http.StatusCreated {
t.Fatalf("create plant status: got %d, want %d; body: %s", plantResponse.Code, http.StatusCreated, plantResponse.Body.String())
}
if got := plantsModel.items[21].GardenID; got != 3 {
t.Errorf("plant garden: got %d, want 3", got)
}
if speciesModel.lastGardenID != 3 {
t.Errorf("species lookup garden: got %d, want 3", speciesModel.lastGardenID)
}
clearSpecies := serveResourceRequest(app, user, http.MethodPatch, "/v1/gardens/3/plants/21", []byte(`{"clear_species_id":true,"clear_acquired_at":true}`))
if clearSpecies.Code != http.StatusOK || plantsModel.items[21].SpeciesID != nil {
t.Fatalf("clear plant species: status=%d body=%s", clearSpecies.Code, clearSpecies.Body.String())
}
foreignGardenID := 4
speciesModel.items[99] = storage.Species{ID: 99, GardenID: &foreignGardenID, CommonName: "Fremd"}
foreignSpeciesResponse := serveResourceRequest(app, user, http.MethodPost, "/v1/gardens/3/plants", []byte(`{"species_id":99,"name":"Nicht erlaubt"}`))
if foreignSpeciesResponse.Code != http.StatusUnprocessableEntity {
t.Fatalf("foreign species status: got %d, want %d; body: %s", foreignSpeciesResponse.Code, http.StatusUnprocessableEntity, foreignSpeciesResponse.Body.String())
}
}
func TestGlobalSpeciesRequiresApplicationPermission(t *testing.T) {
app, _, members := newGardenTestApplication()
user := storage.User{ID: 6, Activated: true}
members.members[[2]int{3, user.ID}] = storage.GardenMember{GardenID: 3, UserID: user.ID, Role: storage.GardenRoleMember}
speciesModel := &speciesTestModel{items: map[int]storage.Species{1: {ID: 1, CommonName: "Global", Version: 1}}}
app.models.Species = speciesModel
showResponse := serveResourceRequest(app, user, http.MethodGet, "/v1/gardens/3/species/1", nil)
if showResponse.Code != http.StatusOK {
t.Fatalf("show global species: got %d, want %d", showResponse.Code, http.StatusOK)
}
updateResponse := serveResourceRequest(app, user, http.MethodPatch, "/v1/gardens/3/species/1", []byte(`{"common_name":"Geändert"}`))
if updateResponse.Code != http.StatusForbidden {
t.Fatalf("update global species: got %d, want %d", updateResponse.Code, http.StatusForbidden)
}
deleteResponse := serveResourceRequest(app, user, http.MethodDelete, "/v1/gardens/3/species/1", nil)
if deleteResponse.Code != http.StatusForbidden {
t.Fatalf("delete global species: got %d, want %d", deleteResponse.Code, http.StatusForbidden)
}
}
func TestAdminCanCreateAndUpdateGlobalSpeciesWithoutGardenWriteRole(t *testing.T) {
app, _, members := newGardenTestApplication()
user := storage.User{ID: 7, Activated: true, Role: storage.ApplicationRoleAdmin}
members.members[[2]int{3, user.ID}] = storage.GardenMember{GardenID: 3, UserID: user.ID, Role: storage.GardenRoleViewer}
speciesModel := &speciesTestModel{items: make(map[int]storage.Species), nextID: 10}
app.models.Species = speciesModel
createResponse := serveResourceRequest(app, user, http.MethodPost, "/v1/gardens/3/species", []byte(`{"common_name":"Tomate","global":true}`))
if createResponse.Code != http.StatusCreated {
t.Fatalf("create global species: got %d, want %d; body: %s", createResponse.Code, http.StatusCreated, createResponse.Body.String())
}
if speciesModel.items[11].GardenID != nil {
t.Fatalf("global species has garden id %v", speciesModel.items[11].GardenID)
}
updateResponse := serveResourceRequest(app, user, http.MethodPatch, "/v1/gardens/3/species/11", []byte(`{"common_name":"Rispentomate"}`))
if updateResponse.Code != http.StatusOK {
t.Fatalf("update global species: got %d, want %d; body: %s", updateResponse.Code, http.StatusOK, updateResponse.Body.String())
}
if got := speciesModel.lastGardenID; got != 0 {
t.Fatalf("global update scope: got %d, want 0", got)
}
deleteResponse := serveResourceRequest(app, user, http.MethodDelete, "/v1/gardens/3/species/11", nil)
if deleteResponse.Code != http.StatusNoContent {
t.Fatalf("delete global species: got %d, want %d; body: %s", deleteResponse.Code, http.StatusNoContent, deleteResponse.Body.String())
}
if got := speciesModel.lastGardenID; got != 0 {
t.Fatalf("global delete scope: got %d, want 0", got)
}
}