251 lines
12 KiB
Go
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)
|
|
}
|
|
}
|