""" Unit tests for pattern API endpoints. Tests the following endpoints: - GET /list_theta_rho_files - GET /list_theta_rho_files_with_metadata - POST /get_theta_rho_coordinates - POST /run_theta_rho (when disconnected) """ import pytest from unittest.mock import patch, MagicMock, AsyncMock import os class TestListThetaRhoFiles: """Tests for /list_theta_rho_files endpoint.""" @pytest.mark.asyncio async def test_list_theta_rho_files(self, async_client): """Test list_theta_rho_files returns list of pattern files.""" mock_files = ["circle.thr", "spiral.thr", "custom/pattern.thr"] with patch("main.pattern_manager.list_theta_rho_files", return_value=mock_files): response = await async_client.get("/list_theta_rho_files") assert response.status_code == 200 data = response.json() assert isinstance(data, list) assert len(data) == 3 assert "circle.thr" in data assert "spiral.thr" in data assert "custom/pattern.thr" in data @pytest.mark.asyncio async def test_list_theta_rho_files_empty(self, async_client): """Test list_theta_rho_files returns empty list when no patterns.""" with patch("main.pattern_manager.list_theta_rho_files", return_value=[]): response = await async_client.get("/list_theta_rho_files") assert response.status_code == 200 data = response.json() assert data == [] class TestListThetaRhoFilesWithMetadata: """Tests for /list_theta_rho_files_with_metadata endpoint.""" @pytest.mark.asyncio async def test_list_theta_rho_files_with_metadata(self, async_client, tmp_path): """Test list_theta_rho_files_with_metadata returns files with metadata.""" mock_files = ["circle.thr"] # The endpoint has two paths: # 1. If metadata_cache.json exists, use it # 2. Otherwise, use ThreadPoolExecutor with process_file # We'll test the fallback path by having the cache file not exist patterns_dir = tmp_path / "patterns" patterns_dir.mkdir() (patterns_dir / "circle.thr").write_text("0 0.5\n1 0.6") with patch("main.pattern_manager.list_theta_rho_files", return_value=mock_files): with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)): # Simulate cache file not existing with patch("builtins.open", side_effect=FileNotFoundError): response = await async_client.get("/list_theta_rho_files_with_metadata") assert response.status_code == 200 data = response.json() assert isinstance(data, list) assert len(data) == 1 # The response structure has 'path', 'name', 'category', 'date_modified', 'coordinates_count' item = data[0] assert item["path"] == "circle.thr" assert item["name"] == "circle" assert "category" in item assert "date_modified" in item assert "coordinates_count" in item class TestGetThetaRhoCoordinates: """Tests for /get_theta_rho_coordinates endpoint.""" @pytest.mark.asyncio async def test_get_theta_rho_coordinates_valid_file(self, async_client, tmp_path): """Test getting coordinates from a valid file.""" # Create test pattern file patterns_dir = tmp_path / "patterns" patterns_dir.mkdir() test_file = patterns_dir / "test.thr" test_file.write_text("0.0 0.5\n1.57 0.8\n3.14 0.3\n") mock_coordinates = [(0.0, 0.5), (1.57, 0.8), (3.14, 0.3)] with patch("main.THETA_RHO_DIR", str(patterns_dir)): with patch("main.parse_theta_rho_file", return_value=mock_coordinates): response = await async_client.post( "/get_theta_rho_coordinates", json={"file_name": "test.thr"} ) assert response.status_code == 200 data = response.json() assert "coordinates" in data assert len(data["coordinates"]) == 3 assert data["coordinates"][0] == [0.0, 0.5] assert data["coordinates"][1] == [1.57, 0.8] assert data["coordinates"][2] == [3.14, 0.3] @pytest.mark.asyncio async def test_get_theta_rho_coordinates_file_not_found(self, async_client, tmp_path): """Test getting coordinates from non-existent file returns error. Note: The endpoint returns 500 because it catches the HTTPException and re-raises it. """ patterns_dir = tmp_path / "patterns" patterns_dir.mkdir() with patch("main.THETA_RHO_DIR", str(patterns_dir)): response = await async_client.post( "/get_theta_rho_coordinates", json={"file_name": "nonexistent.thr"} ) # The endpoint wraps the 404 in a 500 due to exception handling assert response.status_code in [404, 500] data = response.json() assert "not found" in data["detail"].lower() class TestRunThetaRho: """Tests for /run_theta_rho endpoint.""" @pytest.mark.asyncio async def test_run_theta_rho_when_disconnected(self, async_client, mock_state, tmp_path): """Test run_theta_rho fails gracefully when disconnected.""" # The endpoint checks file existence first, then connection patterns_dir = tmp_path / "patterns" patterns_dir.mkdir() test_file = patterns_dir / "circle.thr" test_file.write_text("0 0.5") mock_state.conn = None mock_state.is_homing = False with patch("main.state", mock_state): with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)): response = await async_client.post( "/run_theta_rho", json={ "file_name": "circle.thr", "pre_execution": "none" } ) assert response.status_code == 400 data = response.json() assert "not established" in data["detail"].lower() or "not connected" in data["detail"].lower() @pytest.mark.asyncio async def test_run_theta_rho_during_homing(self, async_client, mock_state, tmp_path): """Test run_theta_rho fails when homing is in progress.""" patterns_dir = tmp_path / "patterns" patterns_dir.mkdir() test_file = patterns_dir / "circle.thr" test_file.write_text("0 0.5") mock_state.is_homing = True mock_state.conn = MagicMock() mock_state.conn.is_connected.return_value = True with patch("main.state", mock_state): with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)): response = await async_client.post( "/run_theta_rho", json={ "file_name": "circle.thr", "pre_execution": "none" } ) assert response.status_code == 409 data = response.json() assert "homing" in data["detail"].lower() @pytest.mark.asyncio async def test_run_theta_rho_file_not_found(self, async_client, mock_state, tmp_path): """Test run_theta_rho returns 404 for non-existent file.""" patterns_dir = tmp_path / "patterns" patterns_dir.mkdir() mock_state.conn = MagicMock() mock_state.conn.is_connected.return_value = True mock_state.is_homing = False with patch("main.state", mock_state): with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)): response = await async_client.post( "/run_theta_rho", json={ "file_name": "nonexistent.thr", "pre_execution": "none" } ) assert response.status_code == 404 data = response.json() assert "not found" in data["detail"].lower() class TestStopExecution: """Tests for /stop_execution endpoint.""" @pytest.mark.asyncio async def test_stop_execution(self, async_client, mock_state): """Test stop_execution endpoint.""" mock_state.is_homing = False mock_state.conn = MagicMock() mock_state.conn.is_connected.return_value = True with patch("main.state", mock_state): with patch("main.pattern_manager.stop_actions", new_callable=AsyncMock, return_value=True): response = await async_client.post("/stop_execution") assert response.status_code == 200 data = response.json() assert data["success"] is True @pytest.mark.asyncio async def test_stop_execution_when_disconnected(self, async_client, mock_state): """Test stop_execution fails when not connected.""" mock_state.conn = None with patch("main.state", mock_state): response = await async_client.post("/stop_execution") assert response.status_code == 400 data = response.json() assert "not established" in data["detail"].lower() class TestPauseResumeExecution: """Tests for /pause_execution and /resume_execution endpoints.""" @pytest.mark.asyncio async def test_pause_execution(self, async_client): """Test pause_execution endpoint.""" with patch("main.pattern_manager.pause_execution", return_value=True): response = await async_client.post("/pause_execution") assert response.status_code == 200 data = response.json() assert data["success"] is True @pytest.mark.asyncio async def test_resume_execution(self, async_client): """Test resume_execution endpoint.""" with patch("main.pattern_manager.resume_execution", return_value=True): response = await async_client.post("/resume_execution") assert response.status_code == 200 data = response.json() assert data["success"] is True class TestDeleteThetaRhoFile: """Tests for /delete_theta_rho_file endpoint.""" @pytest.mark.asyncio async def test_delete_theta_rho_file_success(self, async_client, tmp_path): """Test deleting an existing pattern file.""" patterns_dir = tmp_path / "patterns" patterns_dir.mkdir() test_file = patterns_dir / "test.thr" test_file.write_text("0 0.5") # Must patch pattern_manager.THETA_RHO_DIR which is what the endpoint uses with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)): with patch("modules.core.cache_manager.delete_pattern_cache", return_value=True): response = await async_client.post( "/delete_theta_rho_file", json={"file_name": "test.thr"} ) assert response.status_code == 200 data = response.json() assert data["success"] is True # Verify file was actually deleted assert not test_file.exists() @pytest.mark.asyncio async def test_delete_theta_rho_file_not_found(self, async_client, tmp_path): """Test deleting a non-existent file returns error.""" patterns_dir = tmp_path / "patterns" patterns_dir.mkdir() with patch("main.pattern_manager.THETA_RHO_DIR", str(patterns_dir)): response = await async_client.post( "/delete_theta_rho_file", json={"file_name": "nonexistent.thr"} ) assert response.status_code == 404