cosmoguard-bd/tests/test_cosing_service.py
2025-12-01 19:04:09 +01:00

254 lines
8.7 KiB
Python

"""
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 <empty> tags are removed."""
sample_cosing_response["inciName"] = ["<empty>"]
sample_cosing_response["functionName"] = ["<empty>"]
result = clean_cosing(sample_cosing_response, full=False)
assert "<empty>" 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)