# allow: no-sut-import — black-box HTTP test; drives real routes through the Flask test client """ Tests for chat session lifecycle management. Tests session creation, archiving, deletion, and status transitions. """ import json import pytest class TestSessionCreation: """Tests for session creation scenarios.""" def test_create_session_with_empty_initial_query( self, authenticated_client ): """Test creating a session with empty initial query.""" response = authenticated_client.post( "/api/chat/sessions", json={"initial_query": ""}, content_type="application/json", ) assert response.status_code == 200 data = json.loads(response.data) assert data["success"] is True assert "session_id" in data def test_create_session_with_whitespace_only_query( self, authenticated_client ): """Test creating a session with whitespace-only query.""" response = authenticated_client.post( "/api/chat/sessions", json={"initial_query": " \t\n "}, content_type="application/json", ) assert response.status_code == 200 data = json.loads(response.data) assert data["success"] is True def test_create_session_with_empty_json_object_succeeds( self, authenticated_client ): """An empty JSON object {} creates a session with default values (no initial_query / title). This is what the frontend sends for a blank 'New Chat', so it must succeed.""" response = authenticated_client.post( "/api/chat/sessions", json={}, content_type="application/json", ) assert response.status_code == 200 data = json.loads(response.data) assert data["success"] is True assert "session_id" in data def test_create_session_with_no_body_is_rejected( self, authenticated_client ): """A request with no JSON body is rejected with 400. All chat write endpoints are guarded by @require_json_body, which requires a JSON object. The frontend always sends a body, so this only rejects malformed/empty requests.""" response = authenticated_client.post( "/api/chat/sessions", content_type="application/json", ) assert response.status_code == 400 data = json.loads(response.data) assert data["success"] is False def test_create_multiple_sessions(self, authenticated_client): """Test creating multiple sessions for the same user.""" session_ids = [] for i in range(5): response = authenticated_client.post( "/api/chat/sessions", json={"initial_query": f"Query {i}"}, content_type="application/json", ) assert response.status_code == 200 data = json.loads(response.data) session_ids.append(data["session_id"]) # All session IDs should be unique assert len(session_ids) == len(set(session_ids)) # All sessions should be listable list_response = authenticated_client.get("/api/chat/sessions") data = json.loads(list_response.data) listed_ids = [s["id"] for s in data["sessions"]] for sid in session_ids: assert sid in listed_ids class TestSessionArchiving: """Tests for session archiving functionality.""" def test_archive_session(self, authenticated_client): """Test archiving a session changes its status.""" # Create session create_resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test"}, content_type="application/json", ) session_id = json.loads(create_resp.data)["session_id"] # Archive it archive_resp = authenticated_client.patch( f"/api/chat/sessions/{session_id}", json={"status": "archived"}, content_type="application/json", ) assert archive_resp.status_code == 200 # Verify status changed get_resp = authenticated_client.get(f"/api/chat/sessions/{session_id}") data = json.loads(get_resp.data) assert data["session"]["status"] == "archived" def test_archived_session_not_in_active_list(self, authenticated_client): """Test that archived sessions don't appear in active list.""" # Create session create_resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test"}, content_type="application/json", ) session_id = json.loads(create_resp.data)["session_id"] # Archive it authenticated_client.patch( f"/api/chat/sessions/{session_id}", json={"status": "archived"}, content_type="application/json", ) # Should not be in active list list_resp = authenticated_client.get("/api/chat/sessions?status=active") data = json.loads(list_resp.data) active_ids = [s["id"] for s in data["sessions"]] assert session_id not in active_ids def test_archived_session_in_archived_list(self, authenticated_client): """Test that archived sessions appear in archived list.""" # Create and archive session create_resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test"}, content_type="application/json", ) session_id = json.loads(create_resp.data)["session_id"] authenticated_client.patch( f"/api/chat/sessions/{session_id}", json={"status": "archived"}, content_type="application/json", ) # Should be in archived list list_resp = authenticated_client.get( "/api/chat/sessions?status=archived" ) data = json.loads(list_resp.data) archived_ids = [s["id"] for s in data["sessions"]] assert session_id in archived_ids def test_reactivate_archived_session(self, authenticated_client): """Test that an archived session can be reactivated.""" # Create and archive session create_resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test"}, content_type="application/json", ) session_id = json.loads(create_resp.data)["session_id"] authenticated_client.patch( f"/api/chat/sessions/{session_id}", json={"status": "archived"}, content_type="application/json", ) # Reactivate the session reactivate_resp = authenticated_client.patch( f"/api/chat/sessions/{session_id}", json={"status": "active"}, content_type="application/json", ) assert reactivate_resp.status_code == 200 # Verify status changed back to active get_resp = authenticated_client.get(f"/api/chat/sessions/{session_id}") data = json.loads(get_resp.data) assert data["session"]["status"] == "active" class TestSessionDeletion: """Tests for session deletion functionality.""" def test_delete_via_status_rejected(self, authenticated_client): """Test that PATCH with status=deleted is rejected (use DELETE endpoint).""" # Create session create_resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test"}, content_type="application/json", ) session_id = json.loads(create_resp.data)["session_id"] # Attempt delete via status — should be rejected delete_resp = authenticated_client.patch( f"/api/chat/sessions/{session_id}", json={"status": "deleted"}, content_type="application/json", ) assert delete_resp.status_code == 400 def test_delete_session_via_delete_endpoint(self, authenticated_client): """Test soft-deleting a session via DELETE endpoint.""" # Create session create_resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test"}, content_type="application/json", ) session_id = json.loads(create_resp.data)["session_id"] # Delete via endpoint delete_resp = authenticated_client.delete( f"/api/chat/sessions/{session_id}" ) assert delete_resp.status_code == 200 data = json.loads(delete_resp.data) assert data["success"] is True def test_deleted_session_not_in_active_list(self, authenticated_client): """Test that deleted sessions don't appear in active list.""" # Create and delete session create_resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test"}, content_type="application/json", ) session_id = json.loads(create_resp.data)["session_id"] authenticated_client.delete(f"/api/chat/sessions/{session_id}") # Should not be in active list list_resp = authenticated_client.get("/api/chat/sessions?status=active") data = json.loads(list_resp.data) active_ids = [s["id"] for s in data["sessions"]] assert session_id not in active_ids def test_deleted_session_not_in_all_list(self, authenticated_client): """Test that hard-deleted sessions do not appear in 'all' list.""" # Create and delete session create_resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test"}, content_type="application/json", ) session_id = json.loads(create_resp.data)["session_id"] authenticated_client.delete(f"/api/chat/sessions/{session_id}") # Should NOT be in 'all' list (hard-deleted) list_resp = authenticated_client.get("/api/chat/sessions?status=all") data = json.loads(list_resp.data) all_ids = [s["id"] for s in data["sessions"]] assert session_id not in all_ids def test_delete_nonexistent_session(self, authenticated_client): """Test deleting a session that doesn't exist returns 404.""" response = authenticated_client.delete( "/api/chat/sessions/nonexistent-session-id-12345" ) assert response.status_code == 404 data = json.loads(response.data) assert data["success"] is False class TestSessionTitleUpdates: """Tests for session title update functionality.""" def test_update_title(self, authenticated_client): """Test updating session title.""" # Create session create_resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Original query"}, content_type="application/json", ) session_id = json.loads(create_resp.data)["session_id"] # Update title new_title = "Updated Research Title" update_resp = authenticated_client.patch( f"/api/chat/sessions/{session_id}", json={"title": new_title}, content_type="application/json", ) assert update_resp.status_code == 200 # Verify title changed get_resp = authenticated_client.get(f"/api/chat/sessions/{session_id}") data = json.loads(get_resp.data) assert data["session"]["title"] == new_title def test_update_title_empty_string(self, authenticated_client): """Test updating session title to empty string.""" # Create session create_resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test"}, content_type="application/json", ) session_id = json.loads(create_resp.data)["session_id"] # Update to empty title - should be rejected update_resp = authenticated_client.patch( f"/api/chat/sessions/{session_id}", json={"title": ""}, content_type="application/json", ) assert update_resp.status_code == 400 def test_update_title_too_long(self, authenticated_client): """Test updating session title with too long value.""" # Create session create_resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test"}, content_type="application/json", ) session_id = json.loads(create_resp.data)["session_id"] # Try to update with 501+ character title long_title = "A" * 501 update_resp = authenticated_client.patch( f"/api/chat/sessions/{session_id}", json={"title": long_title}, content_type="application/json", ) assert update_resp.status_code == 400 data = json.loads(update_resp.data) assert "too long" in data["error"].lower() def test_update_title_and_status_together(self, authenticated_client): """Test updating both title and status in one request.""" # Create session create_resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test"}, content_type="application/json", ) session_id = json.loads(create_resp.data)["session_id"] # Update both update_resp = authenticated_client.patch( f"/api/chat/sessions/{session_id}", json={"title": "New Title", "status": "archived"}, content_type="application/json", ) assert update_resp.status_code == 200 # Verify both changed get_resp = authenticated_client.get(f"/api/chat/sessions/{session_id}") data = json.loads(get_resp.data) assert data["session"]["title"] == "New Title" assert data["session"]["status"] == "archived" class TestSessionStatusValidation: """Tests for session status validation.""" def test_invalid_status_rejected(self, authenticated_client): """Test that invalid status values are rejected.""" # Create session create_resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test"}, content_type="application/json", ) session_id = json.loads(create_resp.data)["session_id"] # Try invalid status update_resp = authenticated_client.patch( f"/api/chat/sessions/{session_id}", json={"status": "invalid_status"}, content_type="application/json", ) assert update_resp.status_code == 400 data = json.loads(update_resp.data) assert "invalid" in data["error"].lower() @pytest.mark.parametrize("status", ["active", "archived"]) def test_valid_status_values(self, status, authenticated_client): """Test that valid status values are accepted.""" # Create session create_resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": "Test"}, content_type="application/json", ) session_id = json.loads(create_resp.data)["session_id"] # Update to valid status update_resp = authenticated_client.patch( f"/api/chat/sessions/{session_id}", json={"status": status}, content_type="application/json", ) assert update_resp.status_code == 200 class TestListSessionsPagination: """Tests for session listing pagination.""" def test_list_with_limit(self, authenticated_client): """Test listing sessions with limit.""" # Create 5 sessions for i in range(5): authenticated_client.post( "/api/chat/sessions", json={"initial_query": f"Query {i}"}, content_type="application/json", ) # List with limit response = authenticated_client.get("/api/chat/sessions?limit=3") data = json.loads(response.data) assert len(data["sessions"]) <= 3 def test_list_with_offset(self, authenticated_client): """Test listing sessions with offset.""" # Create 5 sessions created_ids = [] for i in range(5): resp = authenticated_client.post( "/api/chat/sessions", json={"initial_query": f"Query {i}"}, content_type="application/json", ) created_ids.append(json.loads(resp.data)["session_id"]) # Get all sessions all_resp = authenticated_client.get("/api/chat/sessions?limit=100") all_sessions = json.loads(all_resp.data)["sessions"] # Get with offset offset_resp = authenticated_client.get("/api/chat/sessions?offset=2") offset_sessions = json.loads(offset_resp.data)["sessions"] # Offset should skip first 2 assert len(offset_sessions) == len(all_sessions) - 2 def test_list_with_limit_and_offset(self, authenticated_client): """Test listing sessions with both limit and offset.""" # Create 10 sessions for i in range(10): authenticated_client.post( "/api/chat/sessions", json={"initial_query": f"Query {i}"}, content_type="application/json", ) # Get page 2 (offset 3, limit 3) response = authenticated_client.get( "/api/chat/sessions?limit=3&offset=3" ) data = json.loads(response.data) assert len(data["sessions"]) <= 3