254 lines
8.7 KiB
Python
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)
|