aegisai / backend / tests / test_services.py
test_services.py
Raw
"""
Service Layer Tests
Run with: pytest tests/test_services.py -v
"""

import pytest
import asyncio
from datetime import datetime, timedelta
from pathlib import Path
import tempfile

from services.database_service import DatabaseService
from services.action_executor import ActionExecutor


class TestDatabaseService:
    """Test DatabaseService functionality"""
    
    @pytest.fixture
    def temp_db(self):
        """Create temporary database for testing"""
        temp_file = tempfile.NamedTemporaryFile(suffix='.db', delete=False)
        db_path = Path(temp_file.name)
        temp_file.close()
        
        db = DatabaseService(db_path)
        yield db
        
        # Cleanup
        db_path.unlink(missing_ok=True)
    
    def test_database_initialization(self, temp_db):
        """Test database initializes with correct schema"""
        import sqlite3
        
        conn = sqlite3.connect(temp_db.db_path)
        cursor = conn.cursor()
        
        # Check incidents table exists
        cursor.execute("""
            SELECT name FROM sqlite_master 
            WHERE type='table' AND name='incidents'
        """)
        assert cursor.fetchone() is not None
        
        # Check actions table exists
        cursor.execute("""
            SELECT name FROM sqlite_master 
            WHERE type='table' AND name='actions'
        """)
        assert cursor.fetchone() is not None
        
        conn.close()
    
    def test_save_incident(self, temp_db):
        """Test saving incident to database"""
        incident_data = {
            'timestamp': datetime.now().isoformat(),
            'type': 'theft',
            'severity': 'high',
            'confidence': 85,
            'reasoning': 'Test incident',
            'subjects': ['person in dark clothing'],
            'evidence_path': '/path/to/evidence.jpg',
            'response_plan': [
                {'action': 'save_evidence', 'priority': 'immediate'}
            ]
        }
        
        incident_id = temp_db.save_incident(incident_data)
        
        assert incident_id > 0
        
        # Verify it was saved
        retrieved = temp_db.get_incident_by_id(incident_id)
        assert retrieved is not None
        assert retrieved['type'] == 'theft'
        assert retrieved['severity'] == 'high'
        assert retrieved['confidence'] == 85
    
    def test_get_recent_incidents(self, temp_db):
        """Test retrieving recent incidents in reverse chronological order"""
        # Save multiple incidents with increasing timestamps
        base_time = datetime.now()
        for i in range(5):
            temp_db.save_incident({
                'timestamp': (base_time + timedelta(seconds=i)).isoformat(),
                'type': f'type_{i}',
                'severity': 'low',
                'confidence': 70 + i,
                'reasoning': f'Test {i}',
                'subjects': [],
                'evidence_path': '',
                'response_plan': []
            })
        
        incidents = temp_db.get_recent_incidents(limit=10)
        
        assert len(incidents) == 5
        # Should be in reverse chronological order (latest first)
        actual_order = [inc['type'] for inc in incidents]
        expected_order = ['type_4', 'type_3', 'type_2', 'type_1', 'type_0']
        assert actual_order == expected_order
    
    def test_severity_filter(self, temp_db):
        """Test filtering incidents by severity"""
        # Save incidents with different severities
        temp_db.save_incident({
            'timestamp': datetime.now().isoformat(),
            'type': 'low_incident',
            'severity': 'low',
            'confidence': 70,
            'reasoning': 'Test',
            'subjects': [],
            'evidence_path': '',
            'response_plan': []
        })
        
        temp_db.save_incident({
            'timestamp': datetime.now().isoformat(),
            'type': 'high_incident',
            'severity': 'high',
            'confidence': 90,
            'reasoning': 'Test',
            'subjects': [],
            'evidence_path': '',
            'response_plan': []
        })
        
        high_incidents = temp_db.get_recent_incidents(severity='high')
        
        assert len(high_incidents) == 1
        assert high_incidents[0]['type'] == 'high_incident'
    
    def test_save_action(self, temp_db):
        """Test saving action execution"""
        # First create an incident
        incident_id = temp_db.save_incident({
            'timestamp': datetime.now().isoformat(),
            'type': 'test',
            'severity': 'low',
            'confidence': 70,
            'reasoning': 'Test',
            'subjects': [],
            'evidence_path': '',
            'response_plan': []
        })
        
        # Save action
        action_id = temp_db.save_action(
            incident_id=incident_id,
            action_type='save_evidence',
            action_data={'status': 'completed', 'timestamp': datetime.now().isoformat()}
        )
        
        assert action_id > 0
    
    def test_get_statistics(self, temp_db):
        """Test statistics retrieval"""
        # Save some test incidents
        for i in range(3):
            temp_db.save_incident({
                'timestamp': datetime.now().isoformat(),
                'type': 'test',
                'severity': 'high' if i == 0 else 'low',
                'confidence': 80,
                'reasoning': 'Test',
                'subjects': [],
                'evidence_path': '',
                'response_plan': []
            })
        
        stats = temp_db.get_statistics()
        
        assert stats['total_incidents'] == 3
        assert stats['severity_breakdown']['high'] == 1
        assert stats['severity_breakdown']['low'] == 2
    
    def test_update_incident_status(self, temp_db):
        """Test updating incident status"""
        incident_id = temp_db.save_incident({
            'timestamp': datetime.now().isoformat(),
            'type': 'test',
            'severity': 'low',
            'confidence': 70,
            'reasoning': 'Test',
            'subjects': [],
            'evidence_path': '',
            'response_plan': []
        })
        
        success = temp_db.update_incident_status(incident_id, 'resolved')
        assert success
        
        incident = temp_db.get_incident_by_id(incident_id)
        assert incident['status'] == 'resolved'
    
    def test_cleanup_old_incidents(self, temp_db):
        """Test cleanup of old incidents"""
        # Save an incident with timestamp and created_at in the past
        old_time = datetime.now() - timedelta(days=2)
        temp_db.save_incident({
            'timestamp': old_time.isoformat(),
            'type': 'old_test',
            'severity': 'low',
            'confidence': 70,
            'reasoning': 'Old incident',
            'subjects': [],
            'evidence_path': '',
            'response_plan': [],
            'created_at': old_time.isoformat()  # <-- Force old creation date
        })
        
        # Cleanup (should delete incidents older than 1 day)
        deleted = temp_db.cleanup_old_incidents(days=1)
        
        assert deleted >= 1


class TestActionExecutor:
    """Test ActionExecutor functionality"""
    
    @pytest.fixture
    def executor(self):
        """Create ActionExecutor instance"""
        return ActionExecutor()
    
    @pytest.fixture
    def temp_db(self):
        """Create temporary database"""
        temp_file = tempfile.NamedTemporaryFile(suffix='.db', delete=False)
        db_path = Path(temp_file.name)
        temp_file.close()
        
        from services.database_service import DatabaseService
        db = DatabaseService(db_path)
        
        yield db
        
        db_path.unlink(missing_ok=True)
    
    @pytest.mark.asyncio
    async def test_execute_plan(self, executor, temp_db):
        """Test executing a response plan"""
        # Create incident in database
        incident_id = temp_db.save_incident({
            'timestamp': datetime.now().isoformat(),
            'type': 'test',
            'severity': 'high',
            'confidence': 85,
            'reasoning': 'Test',
            'subjects': [],
            'evidence_path': '/test/path.jpg',
            'response_plan': []
        })
        
        plan = [
            {
                'step': 1,
                'action': 'save_evidence',
                'priority': 'immediate',
                'parameters': {},
                'reasoning': 'Preserve evidence'
            },
            {
                'step': 2,
                'action': 'log_incident',
                'priority': 'high',
                'parameters': {},
                'reasoning': 'Document'
            }
        ]
        
        # Execute plan
        await executor.execute_plan(plan, incident_id, '/test/evidence.jpg')
        
        # Should complete without errors
        assert True
    
    @pytest.mark.asyncio
    async def test_save_evidence_action(self, executor):
        """Test save_evidence action"""
        await executor._save_evidence(1, '/test/evidence.jpg', {})
        assert True
    
    @pytest.mark.asyncio
    async def test_log_incident_action(self, executor):
        """Test log_incident action"""
        await executor._log_incident(1, '/test/evidence.jpg', {})
        assert True
    
    @pytest.mark.asyncio
    async def test_send_alert_action(self, executor):
        """Test send_alert action"""
        await executor._send_alert(1, '/test/evidence.jpg', {})
        assert True
    
    @pytest.mark.asyncio
    async def test_lock_door_action(self, executor):
        """Test lock_door action (simulated)"""
        await executor._lock_door(1, '/test/evidence.jpg', {})
        assert True
    
    @pytest.mark.asyncio
    async def test_action_priority_sorting(self, executor, temp_db):
        """Test actions execute in priority order"""
        incident_id = temp_db.save_incident({
            'timestamp': datetime.now().isoformat(),
            'type': 'test',
            'severity': 'high',
            'confidence': 85,
            'reasoning': 'Test',
            'subjects': [],
            'evidence_path': '',
            'response_plan': []
        })
        
        plan = [
            {'step': 3, 'action': 'log_incident', 'priority': 'low', 'parameters': {}, 'reasoning': 'Log'},
            {'step': 1, 'action': 'save_evidence', 'priority': 'immediate', 'parameters': {}, 'reasoning': 'Save'},
            {'step': 2, 'action': 'send_alert', 'priority': 'high', 'parameters': {}, 'reasoning': 'Alert'}
        ]
        
        await executor.execute_plan(plan, incident_id, '/test/evidence.jpg')
        
        # Should execute in priority order: immediate, high, low
        assert True


if __name__ == '__main__':
    pytest.main([__file__, '-v'])