"""Tests for device correlation engine.""" import pytest from datetime import datetime, timedelta from unittest.mock import patch, MagicMock class TestDeviceCorrelator: """Tests for DeviceCorrelator class.""" def test_correlate_same_oui(self): """Test correlation detects same OUI.""" from utils.correlation import DeviceCorrelator correlator = DeviceCorrelator(time_window_seconds=60) wifi_devices = { 'AA:BB:CC:11:22:33': { 'first_seen': datetime.now(), 'last_seen': datetime.now(), 'essid': 'TestNetwork', 'power': -65 } } bt_devices = { 'AA:BB:CC:44:55:66': { 'first_seen': datetime.now(), 'last_seen': datetime.now(), 'name': 'TestPhone', 'rssi': -60 } } correlations = correlator.correlate(wifi_devices, bt_devices) assert len(correlations) >= 1 assert correlations[0]['wifi_mac'] == 'AA:BB:CC:11:22:33' assert correlations[0]['bt_mac'] == 'AA:BB:CC:44:55:66' assert correlations[0]['confidence'] > 0 def test_correlate_timing(self): """Test correlation considers timing.""" from utils.correlation import DeviceCorrelator correlator = DeviceCorrelator(time_window_seconds=30) now = datetime.now() # Devices appearing at the same time wifi_devices = { '11:22:33:44:55:66': { 'first_seen': now, 'last_seen': now, 'essid': 'Network1' } } bt_devices = { '77:88:99:AA:BB:CC': { 'first_seen': now, 'last_seen': now, 'name': 'Device1' } } correlations = correlator.correlate(wifi_devices, bt_devices) # Should have some confidence from timing correlation if correlations: assert correlations[0]['confidence'] > 0 def test_correlate_no_overlap(self): """Test no correlation when devices don't overlap.""" from utils.correlation import DeviceCorrelator correlator = DeviceCorrelator( time_window_seconds=30, min_confidence=0.6 ) now = datetime.now() old = now - timedelta(hours=1) wifi_devices = { '11:22:33:44:55:66': { 'first_seen': old, 'last_seen': old, 'essid': 'OldNetwork' } } bt_devices = { '77:88:99:AA:BB:CC': { 'first_seen': now, 'last_seen': now, 'name': 'NewDevice' } } correlations = correlator.correlate(wifi_devices, bt_devices) # With high min_confidence and no OUI match, should be empty assert len(correlations) == 0 def test_correlate_manufacturer_match(self): """Test correlation boosts confidence for same manufacturer.""" from utils.correlation import DeviceCorrelator correlator = DeviceCorrelator(time_window_seconds=60) now = datetime.now() wifi_devices = { '11:22:33:44:55:66': { 'first_seen': now, 'last_seen': now, 'manufacturer': 'Apple', 'essid': 'Network' } } bt_devices = { '77:88:99:AA:BB:CC': { 'first_seen': now, 'last_seen': now, 'manufacturer': 'Apple', 'name': 'iPhone' } } correlations = correlator.correlate(wifi_devices, bt_devices) # Should have correlation with bonus for manufacturer match assert len(correlations) >= 1 def test_correlate_empty_inputs(self): """Test correlation handles empty inputs.""" from utils.correlation import DeviceCorrelator correlator = DeviceCorrelator() # Empty WiFi assert correlator.correlate({}, {'AA:BB:CC:DD:EE:FF': {}}) == [] # Empty Bluetooth assert correlator.correlate({'AA:BB:CC:DD:EE:FF': {}}, {}) == [] # Both empty assert correlator.correlate({}, {}) == [] def test_correlate_sorting(self): """Test correlations are sorted by confidence.""" from utils.correlation import DeviceCorrelator correlator = DeviceCorrelator( time_window_seconds=60, min_confidence=0.0 ) now = datetime.now() wifi_devices = { 'AA:BB:CC:11:11:11': { 'first_seen': now, 'last_seen': now, 'manufacturer': 'Apple' }, '11:22:33:44:55:66': { 'first_seen': now, 'last_seen': now } } bt_devices = { 'AA:BB:CC:22:22:22': { 'first_seen': now, 'last_seen': now, 'manufacturer': 'Apple' }, '77:88:99:AA:BB:CC': { 'first_seen': now, 'last_seen': now } } correlations = correlator.correlate(wifi_devices, bt_devices) if len(correlations) >= 2: # Should be sorted by confidence (highest first) assert correlations[0]['confidence'] >= correlations[1]['confidence'] class TestGetCorrelations: """Tests for get_correlations function.""" @patch('utils.correlation.correlator') @patch('utils.correlation.db_get_correlations') def test_get_correlations_live(self, mock_db, mock_correlator): """Test get_correlations with live data.""" from utils.correlation import get_correlations mock_correlator.correlate.return_value = [ { 'wifi_mac': 'AA:AA:AA:AA:AA:AA', 'bt_mac': 'BB:BB:BB:BB:BB:BB', 'confidence': 0.8 } ] mock_db.return_value = [] wifi = {'AA:AA:AA:AA:AA:AA': {}} bt = {'BB:BB:BB:BB:BB:BB': {}} results = get_correlations( wifi_devices=wifi, bt_devices=bt, include_historical=False ) assert len(results) == 1 mock_correlator.correlate.assert_called_once() @patch('utils.correlation.correlator') @patch('utils.correlation.db_get_correlations') def test_get_correlations_historical(self, mock_db, mock_correlator): """Test get_correlations includes historical data.""" from utils.correlation import get_correlations mock_correlator.correlate.return_value = [] mock_db.return_value = [ { 'wifi_mac': 'CC:CC:CC:CC:CC:CC', 'bt_mac': 'DD:DD:DD:DD:DD:DD', 'confidence': 0.7, 'first_seen': '2024-01-01', 'last_seen': '2024-01-02' } ] results = get_correlations( wifi_devices={}, bt_devices={}, include_historical=True ) assert len(results) == 1 assert results[0]['wifi_mac'] == 'CC:CC:CC:CC:CC:CC' @patch('utils.correlation.correlator') @patch('utils.correlation.db_get_correlations') def test_get_correlations_deduplication(self, mock_db, mock_correlator): """Test get_correlations deduplicates live and historical.""" from utils.correlation import get_correlations # Same correlation from both sources mock_correlator.correlate.return_value = [ { 'wifi_mac': 'AA:AA:AA:AA:AA:AA', 'bt_mac': 'BB:BB:BB:BB:BB:BB', 'confidence': 0.8 } ] mock_db.return_value = [ { 'wifi_mac': 'AA:AA:AA:AA:AA:AA', 'bt_mac': 'BB:BB:BB:BB:BB:BB', 'confidence': 0.7, 'first_seen': '2024-01-01', 'last_seen': '2024-01-02' } ] wifi = {'AA:AA:AA:AA:AA:AA': {}} bt = {'BB:BB:BB:BB:BB:BB': {}} results = get_correlations( wifi_devices=wifi, bt_devices=bt, include_historical=True ) # Should deduplicate - only one entry for the same device pair matching = [r for r in results if r['wifi_mac'] == 'AA:AA:AA:AA:AA:AA'] assert len(matching) == 1 class TestCorrelationReason: """Tests for correlation reason generation.""" def test_reason_same_oui(self): """Test reason includes OUI match.""" from utils.correlation import DeviceCorrelator correlator = DeviceCorrelator() now = datetime.now() wifi_devices = { 'AA:BB:CC:11:22:33': { 'first_seen': now, 'last_seen': now } } bt_devices = { 'AA:BB:CC:44:55:66': { 'first_seen': now, 'last_seen': now } } correlations = correlator.correlate(wifi_devices, bt_devices) if correlations: assert 'OUI' in correlations[0]['reason'] or 'same' in correlations[0]['reason'].lower() def test_reason_timing(self): """Test reason includes timing information.""" from utils.correlation import DeviceCorrelator correlator = DeviceCorrelator(time_window_seconds=60) now = datetime.now() wifi_devices = { '11:22:33:44:55:66': { 'first_seen': now, 'last_seen': now } } bt_devices = { '77:88:99:AA:BB:CC': { 'first_seen': now + timedelta(seconds=5), 'last_seen': now + timedelta(seconds=5) } } correlations = correlator.correlate(wifi_devices, bt_devices) # If correlation found, should mention timing if correlations and correlations[0]['confidence'] > 0.3: assert 'appeared' in correlations[0]['reason'] or 'timing' in correlations[0]['reason']