package handler import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/elfoundation/hatch/internal/store" "github.com/elfoundation/hatch/internal/testutil" "github.com/go-chi/chi/v5" ) // testSpaceRouter creates a chi router with all routes registered using a fake repo. func testSpaceRouter(repo store.Repository) chi.Router { r := chi.NewRouter() h := New(repo) h.RegisterRoutes(r) return r } func TestCreateSpace(t *testing.T) { repo := testutil.NewFakeRepository() r := testSpaceRouter(repo) body := `{"name": "Test Space", "description": "A test space", "isPublic": true}` req := httptest.NewRequest(http.MethodPost, "/api/spaces", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) } var space store.Space if err := json.NewDecoder(w.Body).Decode(&space); err != nil { t.Fatalf("failed to decode response: %v", err) } if space.Name != "Test Space" { t.Errorf("expected name 'Test Space', got %q", space.Name) } if space.Description != "A test space" { t.Errorf("expected description 'A test space', got %q", space.Description) } if !space.IsPublic { t.Error("expected isPublic to be true") } if space.Slug == "" { t.Error("expected slug to be generated") } } func TestCreateSpaceMissingName(t *testing.T) { repo := testutil.NewFakeRepository() r := testSpaceRouter(repo) body := `{"description": "Missing name"}` req := httptest.NewRequest(http.MethodPost, "/api/spaces", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) } } func TestListPublicSpaces(t *testing.T) { repo := testutil.NewFakeRepository() r := testSpaceRouter(repo) // Create some spaces space1 := &store.Space{ID: "1", Slug: "space-1", Name: "Space 1", IsPublic: true} space2 := &store.Space{ID: "2", Slug: "space-2", Name: "Space 2", IsPublic: false} space3 := &store.Space{ID: "3", Slug: "space-3", Name: "Space 3", IsPublic: true} repo.CreateSpace(nil, space1) repo.CreateSpace(nil, space2) repo.CreateSpace(nil, space3) req := httptest.NewRequest(http.MethodGet, "/api/spaces", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var spaces []*store.Space if err := json.NewDecoder(w.Body).Decode(&spaces); err != nil { t.Fatalf("failed to decode response: %v", err) } if len(spaces) != 2 { t.Fatalf("expected 2 spaces, got %d", len(spaces)) } // Check that only public spaces are returned for _, space := range spaces { if !space.IsPublic { t.Errorf("expected only public spaces, got %q", space.Name) } } } func TestGetSpaceBySlug(t *testing.T) { repo := testutil.NewFakeRepository() r := testSpaceRouter(repo) space := &store.Space{ID: "1", Slug: "test-space", Name: "Test Space", IsPublic: true} repo.CreateSpace(nil, space) req := httptest.NewRequest(http.MethodGet, "/api/spaces/test-space", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var retrievedSpace store.Space if err := json.NewDecoder(w.Body).Decode(&retrievedSpace); err != nil { t.Fatalf("failed to decode response: %v", err) } if retrievedSpace.ID != space.ID { t.Errorf("expected ID %q, got %q", space.ID, retrievedSpace.ID) } if retrievedSpace.Name != space.Name { t.Errorf("expected name %q, got %q", space.Name, retrievedSpace.Name) } } func TestGetSpaceBySlugNotFound(t *testing.T) { repo := testutil.NewFakeRepository() r := testSpaceRouter(repo) req := httptest.NewRequest(http.MethodGet, "/api/spaces/nonexistent", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String()) } } func TestUpdateSpace(t *testing.T) { repo := testutil.NewFakeRepository() r := testSpaceRouter(repo) space := &store.Space{ID: "1", Slug: "test-space", Name: "Original Name", IsPublic: true} repo.CreateSpace(nil, space) body := `{"name": "Updated Name", "isPublic": false}` req := httptest.NewRequest(http.MethodPatch, "/api/spaces/test-space", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var updatedSpace store.Space if err := json.NewDecoder(w.Body).Decode(&updatedSpace); err != nil { t.Fatalf("failed to decode response: %v", err) } if updatedSpace.Name != "Updated Name" { t.Errorf("expected name 'Updated Name', got %q", updatedSpace.Name) } if updatedSpace.IsPublic != false { t.Error("expected isPublic to be false") } } func TestDeleteSpace(t *testing.T) { repo := testutil.NewFakeRepository() r := testSpaceRouter(repo) space := &store.Space{ID: "1", Slug: "test-space", Name: "Test Space", IsPublic: true} repo.CreateSpace(nil, space) req := httptest.NewRequest(http.MethodDelete, "/api/spaces/test-space", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusNoContent { t.Fatalf("expected 204, got %d: %s", w.Code, w.Body.String()) } // Verify space was deleted _, err := repo.GetSpaceBySlug(nil, "test-space") if err == nil { t.Error("expected space to be deleted") } } func TestCreatePhoto(t *testing.T) { repo := testutil.NewFakeRepository() r := testSpaceRouter(repo) space := &store.Space{ID: "1", Slug: "test-space", Name: "Test Space", IsPublic: true} repo.CreateSpace(nil, space) body := `{"url": "https://example.com/photo.jpg", "thumbnailUrl": "https://example.com/thumb.jpg", "faceCount": 2}` req := httptest.NewRequest(http.MethodPost, "/api/spaces/test-space/photos", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) } var photo store.Photo if err := json.NewDecoder(w.Body).Decode(&photo); err != nil { t.Fatalf("failed to decode response: %v", err) } if photo.URL != "https://example.com/photo.jpg" { t.Errorf("expected URL 'https://example.com/photo.jpg', got %q", photo.URL) } if photo.ThumbnailURL != "https://example.com/thumb.jpg" { t.Errorf("expected thumbnail URL 'https://example.com/thumb.jpg', got %q", photo.ThumbnailURL) } if photo.FaceCount != 2 { t.Errorf("expected face count 2, got %d", photo.FaceCount) } } func TestListPhotos(t *testing.T) { repo := testutil.NewFakeRepository() r := testSpaceRouter(repo) space := &store.Space{ID: "1", Slug: "test-space", Name: "Test Space", IsPublic: true} repo.CreateSpace(nil, space) // Create some photos photo1 := &store.Photo{ID: "1", SpaceID: "1", URL: "https://example.com/photo1.jpg"} photo2 := &store.Photo{ID: "2", SpaceID: "1", URL: "https://example.com/photo2.jpg"} photo3 := &store.Photo{ID: "3", SpaceID: "2", URL: "https://example.com/photo3.jpg"} repo.CreatePhoto(nil, photo1) repo.CreatePhoto(nil, photo2) repo.CreatePhoto(nil, photo3) req := httptest.NewRequest(http.MethodGet, "/api/spaces/test-space/photos", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var photos []*store.Photo if err := json.NewDecoder(w.Body).Decode(&photos); err != nil { t.Fatalf("failed to decode response: %v", err) } if len(photos) != 2 { t.Fatalf("expected 2 photos, got %d", len(photos)) } } func TestGuestFaceSearch(t *testing.T) { repo := testutil.NewFakeRepository() r := testSpaceRouter(repo) space := &store.Space{ID: "1", Slug: "test-space", Name: "Test Space", IsPublic: true} repo.CreateSpace(nil, space) body := `{"imageUrl": "https://example.com/test.jpg"}` req := httptest.NewRequest(http.MethodPost, "/api/spaces/test-space/face-search/guest", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.RemoteAddr = "192.168.1.1:12345" w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var response map[string]interface{} if err := json.NewDecoder(w.Body).Decode(&response); err != nil { t.Fatalf("failed to decode response: %v", err) } if response["status"] != "success" { t.Errorf("expected status 'success', got %q", response["status"]) } }