package api import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-chi/chi/v5" ) func newLocationRouter(s *Server) http.Handler { r := chi.NewRouter() r.Get("/api/locations", s.HandleListLocations) r.Post("/api/locations", s.HandleCreateLocation) r.Get("/api/locations/*", s.HandleGetLocation) r.Put("/api/locations/*", s.HandleUpdateLocation) r.Delete("/api/locations/*", s.HandleDeleteLocation) return r } func TestHandleListLocationsEmpty(t *testing.T) { s := newTestServer(t) router := newLocationRouter(s) req := httptest.NewRequest("GET", "/api/locations", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status: got %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) } var locs []LocationResponse if err := json.Unmarshal(w.Body.Bytes(), &locs); err != nil { t.Fatalf("decoding response: %v", err) } if len(locs) != 0 { t.Fatalf("expected 0 locations, got %d", len(locs)) } } func TestHandleCreateAndGetLocation(t *testing.T) { s := newTestServer(t) router := newLocationRouter(s) // Create root location body := `{"path": "lab", "name": "Lab", "location_type": "building"}` req := httptest.NewRequest("POST", "/api/locations", strings.NewReader(body)) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create status: got %d, want %d; body: %s", w.Code, http.StatusCreated, w.Body.String()) } var created LocationResponse if err := json.Unmarshal(w.Body.Bytes(), &created); err != nil { t.Fatalf("decoding create response: %v", err) } if created.Path != "lab" { t.Errorf("path: got %q, want %q", created.Path, "lab") } if created.Name != "Lab" { t.Errorf("name: got %q, want %q", created.Name, "Lab") } if created.Depth != 0 { t.Errorf("depth: got %d, want 0", created.Depth) } if created.ID == "" { t.Error("expected ID to be set") } // Get by path req = httptest.NewRequest("GET", "/api/locations/lab", nil) w = httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("get status: got %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String()) } var got LocationResponse if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { t.Fatalf("decoding get response: %v", err) } if got.ID != created.ID { t.Errorf("ID mismatch: got %q, want %q", got.ID, created.ID) } } func TestHandleCreateNestedLocation(t *testing.T) { s := newTestServer(t) router := newLocationRouter(s) // Create root body := `{"path": "warehouse", "name": "Warehouse", "location_type": "building"}` req := httptest.NewRequest("POST", "/api/locations", strings.NewReader(body)) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create root: got %d; body: %s", w.Code, w.Body.String()) } // Create child body = `{"path": "warehouse/shelf-a", "name": "Shelf A", "location_type": "shelf"}` req = httptest.NewRequest("POST", "/api/locations", strings.NewReader(body)) w = httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create child: got %d; body: %s", w.Code, w.Body.String()) } var child LocationResponse json.Unmarshal(w.Body.Bytes(), &child) if child.Depth != 1 { t.Errorf("child depth: got %d, want 1", child.Depth) } if child.ParentID == nil { t.Error("expected parent_id to be set") } // Create grandchild body = `{"path": "warehouse/shelf-a/bin-3", "name": "Bin 3", "location_type": "bin"}` req = httptest.NewRequest("POST", "/api/locations", strings.NewReader(body)) w = httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create grandchild: got %d; body: %s", w.Code, w.Body.String()) } var gc LocationResponse json.Unmarshal(w.Body.Bytes(), &gc) if gc.Depth != 2 { t.Errorf("grandchild depth: got %d, want 2", gc.Depth) } // Get nested path req = httptest.NewRequest("GET", "/api/locations/warehouse/shelf-a/bin-3", nil) w = httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("get nested: got %d; body: %s", w.Code, w.Body.String()) } } func TestHandleCreateLocationMissingParent(t *testing.T) { s := newTestServer(t) router := newLocationRouter(s) body := `{"path": "nonexistent/child", "name": "Child", "location_type": "shelf"}` req := httptest.NewRequest("POST", "/api/locations", strings.NewReader(body)) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d; body: %s", w.Code, w.Body.String()) } } func TestHandleUpdateLocation(t *testing.T) { s := newTestServer(t) router := newLocationRouter(s) // Create body := `{"path": "office", "name": "Office", "location_type": "room"}` req := httptest.NewRequest("POST", "/api/locations", strings.NewReader(body)) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create: got %d; body: %s", w.Code, w.Body.String()) } // Update body = `{"name": "Main Office", "location_type": "building", "metadata": {"floor": 2}}` req = httptest.NewRequest("PUT", "/api/locations/office", strings.NewReader(body)) w = httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("update: got %d; body: %s", w.Code, w.Body.String()) } var updated LocationResponse json.Unmarshal(w.Body.Bytes(), &updated) if updated.Name != "Main Office" { t.Errorf("name: got %q, want %q", updated.Name, "Main Office") } if updated.LocationType != "building" { t.Errorf("type: got %q, want %q", updated.LocationType, "building") } } func TestHandleDeleteLocation(t *testing.T) { s := newTestServer(t) router := newLocationRouter(s) // Create body := `{"path": "temp", "name": "Temp", "location_type": "area"}` req := httptest.NewRequest("POST", "/api/locations", strings.NewReader(body)) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create: got %d; body: %s", w.Code, w.Body.String()) } // Delete req = httptest.NewRequest("DELETE", "/api/locations/temp", nil) w = httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNoContent { t.Fatalf("delete: got %d, want %d; body: %s", w.Code, http.StatusNoContent, w.Body.String()) } // Verify gone req = httptest.NewRequest("GET", "/api/locations/temp", nil) w = httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("get after delete: got %d, want %d", w.Code, http.StatusNotFound) } } func TestHandleDeleteLocationNotFound(t *testing.T) { s := newTestServer(t) router := newLocationRouter(s) req := httptest.NewRequest("DELETE", "/api/locations/doesnotexist", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("delete missing: got %d, want %d; body: %s", w.Code, http.StatusNotFound, w.Body.String()) } } func TestHandleListLocationsTree(t *testing.T) { s := newTestServer(t) router := newLocationRouter(s) // Create hierarchy for _, loc := range []string{ `{"path": "site", "name": "Site", "location_type": "site"}`, `{"path": "site/bldg", "name": "Building", "location_type": "building"}`, `{"path": "site/bldg/room1", "name": "Room 1", "location_type": "room"}`, `{"path": "other", "name": "Other", "location_type": "site"}`, } { req := httptest.NewRequest("POST", "/api/locations", strings.NewReader(loc)) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create: got %d; body: %s", w.Code, w.Body.String()) } } // List tree under "site" req := httptest.NewRequest("GET", "/api/locations?tree=site", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("tree: got %d; body: %s", w.Code, w.Body.String()) } var locs []LocationResponse json.Unmarshal(w.Body.Bytes(), &locs) if len(locs) != 3 { t.Fatalf("tree count: got %d, want 3 (site + bldg + room1)", len(locs)) } // Full list should have 4 req = httptest.NewRequest("GET", "/api/locations", nil) w = httptest.NewRecorder() router.ServeHTTP(w, req) json.Unmarshal(w.Body.Bytes(), &locs) if len(locs) != 4 { t.Fatalf("full list: got %d, want 4", len(locs)) } } func TestHandleCreateLocationDuplicate(t *testing.T) { s := newTestServer(t) router := newLocationRouter(s) body := `{"path": "dup", "name": "Dup", "location_type": "area"}` req := httptest.NewRequest("POST", "/api/locations", strings.NewReader(body)) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("first create: got %d; body: %s", w.Code, w.Body.String()) } // Duplicate req = httptest.NewRequest("POST", "/api/locations", strings.NewReader(body)) w = httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusConflict { t.Fatalf("duplicate: got %d, want %d; body: %s", w.Code, http.StatusConflict, w.Body.String()) } } func TestHandleCreateLocationValidation(t *testing.T) { s := newTestServer(t) router := newLocationRouter(s) tests := []struct { name string body string }{ {"missing path", `{"name": "X", "location_type": "area"}`}, {"missing name", `{"path": "x", "location_type": "area"}`}, {"missing type", `{"path": "x", "name": "X"}`}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest("POST", "/api/locations", strings.NewReader(tc.body)) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("got %d, want 400; body: %s", w.Code, w.Body.String()) } }) } }