# tests/unit/test_neo.py # Unit tests for libs/neo.py from datetime import datetime from unittest.mock import AsyncMock, Mock, patch import pytest from libs.neo import Neo4jClient, SHACLValidator, TemporalQueries # pylint: disable=wrong-import-position,import-error,too-few-public-methods,global-statement # pylint: disable=raise-missing-from,unused-argument,too-many-arguments,too-many-positional-arguments # pylint: disable=too-many-locals,import-outside-toplevel # mypy: disable-error-code=union-attr # mypy: disable-error-code=no-untyped-def class TestNeo4jClient: """Test Neo4jClient class""" def test_neo4j_client_init(self): """Test Neo4jClient initialization""" mock_driver = Mock() client = Neo4jClient(mock_driver) assert client.driver == mock_driver @pytest.mark.asyncio async def test_close(self): """Test closing the driver""" mock_driver = Mock() mock_driver.close = Mock() client = Neo4jClient(mock_driver) with patch("asyncio.get_event_loop") as mock_get_loop: mock_loop = Mock() mock_get_loop.return_value = mock_loop mock_loop.run_in_executor = AsyncMock() await client.close() mock_loop.run_in_executor.assert_called_once_with(None, mock_driver.close) @pytest.mark.asyncio async def test_run_query_success(self): """Test successful query execution""" mock_driver = Mock() mock_session = Mock() mock_result = Mock() mock_record = Mock() mock_record.data.return_value = {"name": "test", "value": 123} mock_result.__iter__ = Mock(return_value=iter([mock_record])) mock_session.run.return_value = mock_result mock_driver.session.return_value.__enter__ = Mock(return_value=mock_session) mock_driver.session.return_value.__exit__ = Mock(return_value=None) client = Neo4jClient(mock_driver) with patch("asyncio.get_event_loop") as mock_get_loop: mock_loop = Mock() mock_get_loop.return_value = mock_loop mock_loop.run_in_executor = AsyncMock( return_value=[{"name": "test", "value": 123}] ) result = await client.run_query("MATCH (n) RETURN n", {"param": "value"}) assert result == [{"name": "test", "value": 123}] mock_loop.run_in_executor.assert_called_once() @pytest.mark.asyncio async def test_run_query_with_retries(self): """Test query execution with retries on transient errors""" from neo4j.exceptions import TransientError mock_driver = Mock() client = Neo4jClient(mock_driver) with ( patch("asyncio.get_event_loop") as mock_get_loop, patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep, ): mock_loop = Mock() mock_get_loop.return_value = mock_loop # First two calls fail, third succeeds mock_loop.run_in_executor = AsyncMock( side_effect=[ TransientError("Connection lost"), TransientError("Connection lost"), [{"result": "success"}], ] ) result = await client.run_query("MATCH (n) RETURN n", max_retries=3) assert result == [{"result": "success"}] assert mock_loop.run_in_executor.call_count == 3 assert mock_sleep.call_count == 2 # Two retries @pytest.mark.asyncio async def test_run_query_max_retries_exceeded(self): """Test query execution when max retries exceeded""" from neo4j.exceptions import TransientError mock_driver = Mock() client = Neo4jClient(mock_driver) with ( patch("asyncio.get_event_loop") as mock_get_loop, patch("asyncio.sleep", new_callable=AsyncMock), ): mock_loop = Mock() mock_get_loop.return_value = mock_loop mock_loop.run_in_executor = AsyncMock( side_effect=TransientError("Connection lost") ) with pytest.raises(TransientError): await client.run_query("MATCH (n) RETURN n", max_retries=2) assert mock_loop.run_in_executor.call_count == 2 @pytest.mark.asyncio async def test_run_query_non_retryable_error(self): """Test query execution with non-retryable error""" mock_driver = Mock() client = Neo4jClient(mock_driver) with patch("asyncio.get_event_loop") as mock_get_loop: mock_loop = Mock() mock_get_loop.return_value = mock_loop mock_loop.run_in_executor = AsyncMock( side_effect=ValueError("Invalid query") ) with pytest.raises(ValueError): await client.run_query("INVALID QUERY") assert mock_loop.run_in_executor.call_count == 1 # No retries @pytest.mark.asyncio async def test_run_transaction_success(self): """Test successful transaction execution""" mock_driver = Mock() client = Neo4jClient(mock_driver) def mock_transaction_func(tx): return {"created": "node"} with patch("asyncio.get_event_loop") as mock_get_loop: mock_loop = Mock() mock_get_loop.return_value = mock_loop mock_loop.run_in_executor = AsyncMock(return_value={"created": "node"}) result = await client.run_transaction(mock_transaction_func) assert result == {"created": "node"} mock_loop.run_in_executor.assert_called_once() @pytest.mark.asyncio async def test_create_node(self): """Test node creation with temporal properties""" mock_driver = Mock() client = Neo4jClient(mock_driver) properties = {"name": "Test Node", "value": 123} with patch.object(client, "run_query") as mock_run_query: mock_run_query.return_value = [ { "n": { "name": "Test Node", "value": 123, "asserted_at": "2023-01-01T00:00:00", } } ] result = await client.create_node("TestLabel", properties) assert result == { "name": "Test Node", "value": 123, "asserted_at": "2023-01-01T00:00:00", } mock_run_query.assert_called_once() # Check that asserted_at was added to properties call_args = mock_run_query.call_args assert "asserted_at" in call_args[0][1]["properties"] @pytest.mark.asyncio async def test_create_node_with_existing_asserted_at(self): """Test node creation when asserted_at already exists""" mock_driver = Mock() client = Neo4jClient(mock_driver) existing_time = datetime(2023, 1, 1, 12, 0, 0) properties = {"name": "Test Node", "asserted_at": existing_time} with patch.object(client, "run_query") as mock_run_query: mock_run_query.return_value = [{"n": properties}] result = await client.create_node("TestLabel", properties) # Should not modify existing asserted_at call_args = mock_run_query.call_args assert call_args[0][1]["properties"]["asserted_at"] == existing_time @pytest.mark.asyncio async def test_update_node(self): """Test node update with bitemporal versioning""" mock_driver = Mock() client = Neo4jClient(mock_driver) properties = {"name": "Updated Node", "value": 456} with patch.object(client, "run_transaction") as mock_run_transaction: mock_run_transaction.return_value = {"name": "Updated Node", "value": 456} result = await client.update_node("TestLabel", "node123", properties) assert result == {"name": "Updated Node", "value": 456} mock_run_transaction.assert_called_once() @pytest.mark.asyncio async def test_create_relationship(self): """Test relationship creation""" mock_driver = Mock() client = Neo4jClient(mock_driver) rel_properties = {"strength": 0.8, "type": "RELATED_TO"} with patch.object(client, "run_query") as mock_run_query: mock_run_query.return_value = [{"r": rel_properties}] result = await client.create_relationship( "Person", "person1", "Company", "company1", "WORKS_FOR", rel_properties ) assert result == rel_properties mock_run_query.assert_called_once() # Check query parameters call_args = mock_run_query.call_args params = call_args[0][1] assert params["from_id"] == "person1" assert params["to_id"] == "company1" assert "asserted_at" in params["properties"] @pytest.mark.asyncio async def test_get_node_lineage(self): """Test getting node lineage""" mock_driver = Mock() client = Neo4jClient(mock_driver) lineage_data = [ {"path": "path1", "evidence": {"id": "evidence1"}}, {"path": "path2", "evidence": {"id": "evidence2"}}, ] with patch.object(client, "run_query") as mock_run_query: mock_run_query.return_value = lineage_data result = await client.get_node_lineage("node123", max_depth=5) assert result == lineage_data mock_run_query.assert_called_once() # Check query parameters call_args = mock_run_query.call_args params = call_args[0][1] assert params["node_id"] == "node123" assert params["max_depth"] == 5 @pytest.mark.asyncio async def test_export_to_rdf_success(self): """Test successful RDF export""" mock_driver = Mock() client = Neo4jClient(mock_driver) export_result = [{"triplesCount": 100, "format": "turtle"}] with patch.object(client, "run_query") as mock_run_query: mock_run_query.return_value = export_result result = await client.export_to_rdf("turtle") assert result == {"triplesCount": 100, "format": "turtle"} mock_run_query.assert_called_once() @pytest.mark.asyncio async def test_export_to_rdf_fallback(self): """Test RDF export with fallback""" mock_driver = Mock() client = Neo4jClient(mock_driver) with ( patch.object(client, "run_query") as mock_run_query, patch.object(client, "_export_rdf_fallback") as mock_fallback, ): mock_run_query.side_effect = Exception("n10s plugin not available") mock_fallback.return_value = "fallback_rdf_data" result = await client.export_to_rdf("turtle") assert result == {"rdf_data": "fallback_rdf_data", "format": "turtle"} mock_fallback.assert_called_once_with("neo4j") @pytest.mark.asyncio async def test_export_rdf_fallback(self): """Test fallback RDF export method""" mock_driver = Mock() client = Neo4jClient(mock_driver) nodes_data = [ {"labels": ["Person"], "props": {"name": "John"}, "neo_id": 1}, {"labels": ["Company"], "props": {"name": "Acme"}, "neo_id": 2}, ] rels_data = [{"type": "WORKS_FOR", "props": {}, "from_id": 1, "to_id": 2}] with patch.object(client, "run_query") as mock_run_query: mock_run_query.side_effect = [nodes_data, rels_data] result = await client._export_rdf_fallback() assert isinstance(result, str) assert ( "Person" in result or "Company" in result ) # Should contain some RDF data assert mock_run_query.call_count == 2 class TestSHACLValidator: """Test SHACLValidator class""" def test_shacl_validator_init(self): """Test SHACLValidator initialization""" validator = SHACLValidator("/path/to/shapes.ttl") assert validator.shapes_file == "/path/to/shapes.ttl" @pytest.mark.asyncio async def test_validate_graph_success(self): """Test successful SHACL validation""" validator = SHACLValidator("/path/to/shapes.ttl") rdf_data = """ @prefix ex: . ex:person1 a ex:Person ; ex:name "John Doe" ; ex:age 30 . """ def mock_validate(): # Mock pySHACL validation with ( patch("pyshacl.validate") as mock_pyshacl, patch("rdflib.Graph") as mock_graph_class, ): mock_data_graph = Mock() mock_shapes_graph = Mock() mock_results_graph = Mock() mock_results_graph.subjects.return_value = [] # No violations mock_graph_class.side_effect = [mock_data_graph, mock_shapes_graph] mock_pyshacl.return_value = ( True, mock_results_graph, "Validation passed", ) return validator._SHACLValidator__validate_sync(rdf_data) with patch("asyncio.get_event_loop") as mock_get_loop: mock_loop = Mock() mock_get_loop.return_value = mock_loop mock_loop.run_in_executor = AsyncMock( return_value={ "conforms": True, "results_text": "Validation passed", "violations_count": 0, } ) result = await validator.validate_graph(rdf_data) assert result["conforms"] is True assert result["violations_count"] == 0 assert "passed" in result["results_text"] @pytest.mark.asyncio async def test_validate_graph_with_violations(self): """Test SHACL validation with violations""" validator = SHACLValidator("/path/to/shapes.ttl") rdf_data = """ @prefix ex: . ex:person1 a ex:Person ; ex:name "John Doe" . """ with patch("asyncio.get_event_loop") as mock_get_loop: mock_loop = Mock() mock_get_loop.return_value = mock_loop mock_loop.run_in_executor = AsyncMock( return_value={ "conforms": False, "results_text": "Missing required property: age", "violations_count": 1, } ) result = await validator.validate_graph(rdf_data) assert result["conforms"] is False assert result["violations_count"] == 1 assert "Missing" in result["results_text"] @pytest.mark.asyncio async def test_validate_graph_import_error(self): """Test SHACL validation when pySHACL not available""" validator = SHACLValidator("/path/to/shapes.ttl") with patch("asyncio.get_event_loop") as mock_get_loop: mock_loop = Mock() mock_get_loop.return_value = mock_loop mock_loop.run_in_executor = AsyncMock( return_value={ "conforms": True, "results_text": "SHACL validation skipped (pySHACL not installed)", "violations_count": 0, } ) result = await validator.validate_graph( "@prefix ex: ." ) assert result["conforms"] is True assert result["violations_count"] == 0 assert "skipped" in result["results_text"] @pytest.mark.asyncio async def test_validate_graph_validation_error(self): """Test SHACL validation with validation error""" validator = SHACLValidator("/path/to/shapes.ttl") with patch("asyncio.get_event_loop") as mock_get_loop: mock_loop = Mock() mock_get_loop.return_value = mock_loop mock_loop.run_in_executor = AsyncMock( return_value={ "conforms": False, "results_text": "Validation error: Invalid RDF syntax", "violations_count": -1, } ) result = await validator.validate_graph("invalid rdf data") assert result["conforms"] is False assert result["violations_count"] == -1 assert "error" in result["results_text"] class TestTemporalQueries: """Test TemporalQueries class""" def test_get_current_state_query_no_filters(self): """Test current state query without filters""" query = TemporalQueries.get_current_state_query("Person") assert "MATCH (n:Person)" in query assert "n.retracted_at IS NULL" in query assert "ORDER BY n.asserted_at DESC" in query def test_get_current_state_query_with_filters(self): """Test current state query with filters""" filters = {"name": "John Doe", "age": 30, "active": True} query = TemporalQueries.get_current_state_query("Person", filters) assert "MATCH (n:Person)" in query assert "n.retracted_at IS NULL" in query assert "n.name = 'John Doe'" in query assert "n.age = 30" in query assert "n.active = True" in query def test_get_historical_state_query_no_filters(self): """Test historical state query without filters""" as_of_time = datetime(2023, 6, 15, 12, 0, 0) query = TemporalQueries.get_historical_state_query("Person", as_of_time) assert "MATCH (n:Person)" in query assert "n.asserted_at <= datetime('2023-06-15T12:00:00')" in query assert ( "n.retracted_at IS NULL OR n.retracted_at > datetime('2023-06-15T12:00:00')" in query ) assert "ORDER BY n.asserted_at DESC" in query def test_get_historical_state_query_with_filters(self): """Test historical state query with filters""" as_of_time = datetime(2023, 6, 15, 12, 0, 0) filters = {"department": "Engineering", "level": 5} query = TemporalQueries.get_historical_state_query( "Employee", as_of_time, filters ) assert "MATCH (n:Employee)" in query assert "n.asserted_at <= datetime('2023-06-15T12:00:00')" in query assert "n.department = 'Engineering'" in query assert "n.level = 5" in query def test_get_audit_trail_query(self): """Test audit trail query""" query = TemporalQueries.get_audit_trail_query("node123") assert "MATCH (n {id: 'node123'})" in query assert "n.asserted_at as asserted_at" in query assert "n.retracted_at as retracted_at" in query assert "n.source as source" in query assert "n.extractor_version as extractor_version" in query assert "properties(n) as properties" in query assert "ORDER BY n.asserted_at ASC" in query class TestIntegration: """Test integration scenarios""" @pytest.mark.asyncio async def test_full_neo4j_workflow(self): """Test complete Neo4j workflow""" mock_driver = Mock() client = Neo4jClient(mock_driver) # Mock all the operations with ( patch.object(client, "create_node") as mock_create, patch.object(client, "create_relationship") as mock_create_rel, patch.object(client, "get_node_lineage") as mock_lineage, ): mock_create.return_value = {"id": "person1", "name": "John Doe"} mock_create_rel.return_value = {"type": "WORKS_FOR", "strength": 0.8} mock_lineage.return_value = [{"path": "lineage_path"}] # Create nodes person = await client.create_node("Person", {"name": "John Doe"}) company = await client.create_node("Company", {"name": "Acme Corp"}) # Create relationship relationship = await client.create_relationship( "Person", "person1", "Company", "company1", "WORKS_FOR", {"strength": 0.8}, ) # Get lineage lineage = await client.get_node_lineage("person1") assert person["name"] == "John Doe" assert relationship["type"] == "WORKS_FOR" assert len(lineage) == 1 @pytest.mark.asyncio async def test_temporal_queries_integration(self): """Test temporal queries integration""" mock_driver = Mock() client = Neo4jClient(mock_driver) # Test current state query current_query = TemporalQueries.get_current_state_query( "Person", {"active": True} ) assert "Person" in current_query assert "active = True" in current_query # Test historical state query historical_time = datetime(2023, 1, 1, 0, 0, 0) historical_query = TemporalQueries.get_historical_state_query( "Person", historical_time ) assert "2023-01-01T00:00:00" in historical_query # Test audit trail query audit_query = TemporalQueries.get_audit_trail_query("person123") assert "person123" in audit_query @pytest.mark.asyncio async def test_shacl_validation_integration(self): """Test SHACL validation integration""" validator = SHACLValidator("/path/to/shapes.ttl") # Mock the validation process with patch("asyncio.get_event_loop") as mock_get_loop: mock_loop = Mock() mock_get_loop.return_value = mock_loop mock_loop.run_in_executor = AsyncMock( return_value={ "conforms": True, "results_text": "All constraints satisfied", "violations_count": 0, } ) rdf_data = "@prefix ex: . ex:person1 a ex:Person ." result = await validator.validate_graph(rdf_data) assert result["conforms"] is True assert result["violations_count"] == 0