""" Tests for COSING Service Test coverage: - parse_cas_numbers: CAS number parsing logic - cosing_search: API search functionality - clean_cosing: JSON cleaning and formatting """ import pytest from unittest.mock import Mock, patch from pif_compiler.services.srv_cosing import ( parse_cas_numbers, cosing_search, clean_cosing, ) class TestParseCasNumbers: """Test CAS number parsing function.""" def test_single_cas_number(self): """Test parsing a single CAS number.""" result = parse_cas_numbers(["7732-18-5"]) assert result == ["7732-18-5"] def test_multiple_cas_with_slash(self): """Test parsing multiple CAS numbers separated by slash.""" result = parse_cas_numbers(["7732-18-5/56-81-5"]) assert result == ["7732-18-5", "56-81-5"] def test_multiple_cas_with_semicolon(self): """Test parsing multiple CAS numbers separated by semicolon.""" result = parse_cas_numbers(["7732-18-5;56-81-5"]) assert result == ["7732-18-5", "56-81-5"] def test_multiple_cas_with_comma(self): """Test parsing multiple CAS numbers separated by comma.""" result = parse_cas_numbers(["7732-18-5,56-81-5"]) assert result == ["7732-18-5", "56-81-5"] def test_double_dash_separator(self): """Test parsing CAS numbers with double dash separator.""" result = parse_cas_numbers(["7732-18-5--56-81-5"]) assert result == ["7732-18-5", "56-81-5"] def test_cas_with_parentheses(self): """Test that parenthetical info is removed.""" result = parse_cas_numbers(["7732-18-5 (hydrate)"]) assert result == ["7732-18-5"] def test_cas_with_extra_whitespace(self): """Test that extra whitespace is trimmed.""" result = parse_cas_numbers([" 7732-18-5 / 56-81-5 "]) assert result == ["7732-18-5", "56-81-5"] def test_removes_invalid_dash(self): """Test that standalone dashes are removed.""" result = parse_cas_numbers(["7732-18-5/-/56-81-5"]) assert result == ["7732-18-5", "56-81-5"] def test_complex_mixed_separators(self): """Test with multiple separator types.""" result = parse_cas_numbers(["7732-18-5/56-81-5;50-00-0"]) assert result == ["7732-18-5", "56-81-5", "50-00-0"] class TestCosingSearch: """Test COSING API search functionality.""" @patch('pif_compiler.services.cosing_service.req.post') def test_search_by_name_success(self, mock_post): """Test successful search by ingredient name.""" # Mock API response mock_response = Mock() mock_response.json.return_value = { "results": [{ "metadata": { "inciName": ["WATER"], "casNo": ["7732-18-5"], "substanceId": ["12345"] } }] } mock_post.return_value = mock_response result = cosing_search("WATER", mode="name") assert result is not None assert result["inciName"] == ["WATER"] assert result["casNo"] == ["7732-18-5"] @patch('pif_compiler.services.cosing_service.req.post') def test_search_by_cas_success(self, mock_post): """Test successful search by CAS number.""" mock_response = Mock() mock_response.json.return_value = { "results": [{ "metadata": { "inciName": ["WATER"], "casNo": ["7732-18-5"] } }] } mock_post.return_value = mock_response result = cosing_search("7732-18-5", mode="cas") assert result is not None assert "7732-18-5" in result["casNo"] @patch('pif_compiler.services.cosing_service.req.post') def test_search_by_ec_success(self, mock_post): """Test successful search by EC number.""" mock_response = Mock() mock_response.json.return_value = { "results": [{ "metadata": { "ecNo": ["231-791-2"] } }] } mock_post.return_value = mock_response result = cosing_search("231-791-2", mode="ec") assert result is not None assert "231-791-2" in result["ecNo"] @patch('pif_compiler.services.cosing_service.req.post') def test_search_by_id_success(self, mock_post): """Test successful search by substance ID.""" mock_response = Mock() mock_response.json.return_value = { "results": [{ "metadata": { "substanceId": ["12345"] } }] } mock_post.return_value = mock_response result = cosing_search("12345", mode="id") assert result is not None assert result["substanceId"] == ["12345"] @patch('pif_compiler.services.cosing_service.req.post') def test_search_no_results(self, mock_post): """Test search with no results returns status code.""" mock_response = Mock() mock_response.json.return_value = {"results": []} mock_post.return_value = mock_response result = cosing_search("NONEXISTENT", mode="name") assert result == None # Should return None def test_search_invalid_mode(self): """Test that invalid mode raises ValueError.""" with pytest.raises(ValueError): cosing_search("WATER", mode="invalid_mode") class TestCleanCosing: """Test COSING JSON cleaning function.""" def test_clean_basic_fields(self, sample_cosing_response): """Test cleaning basic string and list fields.""" result = clean_cosing(sample_cosing_response, full=False) assert result["inciName"] == "WATER" assert result["casNo"] == ["7732-18-5"] assert result["ecNo"] == ["231-791-2"] def test_removes_empty_tags(self, sample_cosing_response): """Test that tags are removed.""" sample_cosing_response["inciName"] = [""] sample_cosing_response["functionName"] = [""] result = clean_cosing(sample_cosing_response, full=False) assert "" not in result["inciName"] assert result["functionName"] == [] def test_parses_cas_numbers(self, sample_cosing_response): """Test that CAS numbers are parsed correctly.""" sample_cosing_response["casNo"] = ["56-81-5"] result = clean_cosing(sample_cosing_response, full=False) assert result["casNo"] == ["56-81-5"] def test_creates_cosing_url(self, sample_cosing_response): """Test that COSING URL is created.""" result = clean_cosing(sample_cosing_response, full=False) assert "cosingUrl" in result assert "12345" in result["cosingUrl"] assert result["cosingUrl"] == "https://ec.europa.eu/growth/tools-databases/cosing/details/12345" def test_renames_common_name(self, sample_cosing_response): """Test that nameOfCommonIngredientsGlossary is renamed.""" result = clean_cosing(sample_cosing_response, full=False) assert "commonName" in result assert result["commonName"] == "Water" assert "nameOfCommonIngredientsGlossary" not in result def test_empty_lists_handled(self, sample_cosing_response): """Test that empty lists are handled correctly.""" sample_cosing_response["inciName"] = [] sample_cosing_response["casNo"] = [] result = clean_cosing(sample_cosing_response, full=False) assert result["inciName"] == "" assert result["casNo"] == [] class TestIntegration: """Integration tests with real API (marked as slow).""" @pytest.mark.integration def test_real_water_search(self): """Test real API call for WATER (requires internet).""" result = cosing_search("WATER", mode="name") if result and isinstance(result, dict): # Real API call succeeded assert "inciName" in result or "casNo" in result @pytest.mark.integration def test_real_cas_search(self): """Test real API call by CAS number (requires internet).""" result = cosing_search("56-81-5", mode="cas") if result and isinstance(result, dict): assert "casNo" in result @pytest.mark.integration def test_full_workflow(self): """Test complete workflow: search -> clean.""" # Search for glycerin raw_result = cosing_search("GLYCERIN", mode="name") if raw_result and isinstance(raw_result, dict): # Clean the result clean_result = clean_cosing(raw_result, full=False) # Verify cleaned structure assert "cosingUrl" in clean_result assert isinstance(clean_result.get("casNo"), list)