diff --git a/.gitignore b/.gitignore index de285a7..dcac9f3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ env/ venv/ .venv/ ENV/ +uv.lock # Logs *.log diff --git a/pyproject.toml b/pyproject.toml index fcd42e1..9ea365b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ Issues = "https://github.com/smittix/intercept/issues" dev = [ "pytest>=7.0.0", "pytest-cov>=4.0.0", + "pytest-mock>=3.15.1", "ruff>=0.1.0", "black>=23.0.0", "mypy>=1.0.0", diff --git a/requirements-dev.txt b/requirements-dev.txt index 07180d4..8051f92 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,6 +4,7 @@ # Testing pytest>=7.0.0 pytest-cov>=4.0.0 +pytest-mock>=3.15.1 # Code quality ruff>=0.1.0 diff --git a/tests/test_bluetooth.py b/tests/test_bluetooth.py new file mode 100644 index 0000000..be91d8c --- /dev/null +++ b/tests/test_bluetooth.py @@ -0,0 +1,129 @@ +import pytest +import json +import subprocess +from unittest.mock import MagicMock, patch +from flask import Flask +from routes.bluetooth import bluetooth_bp, classify_bt_device, detect_tracker + + +@pytest.fixture(autouse=True) +def mock_app_module(mocker): + mock_app = mocker.patch("routes.bluetooth.app_module") + mock_app.bt_devices = {} + mock_app.bt_beacons = {} + mock_app.bt_services = {} + mock_app.bt_queue = MagicMock() + mock_app.bt_lock = MagicMock() + mock_app.bt_process = None + mock_app.bt_interface = "hci0" + return mock_app + + +@pytest.fixture +def app(): + app = Flask(__name__) + app.register_blueprint(bluetooth_bp) + return app + + +@pytest.fixture +def client(app): + return app.test_client() + + +def test_classify_bt_device_by_name(): + """Test classification based on common naming patterns.""" + assert classify_bt_device("Sony WH-1000XM4", None, None) == "audio" + assert classify_bt_device("iPhone 15", None, None) == "phone" + assert classify_bt_device("Garmin Fenix", None, None) == "wearable" + assert classify_bt_device("Microsoft Mouse", None, None) == "input" + assert classify_bt_device("AirTag", None, None) == "tracker" + assert classify_bt_device("Generic Device", None, None) == "other" + + +def test_classify_bt_device_by_class(): + """Test classification based on Bluetooth Class of Device (CoD).""" + assert classify_bt_device(None, 0x0100, None) == "computer" + assert classify_bt_device(None, 0x0200, None) == "phone" + assert classify_bt_device(None, 0x0400, None) == "audio" + + +def test_detect_tracker_by_mac(): + """Test tracker detection using MAC OUI prefixes.""" + # Assuming 'FF:FF:FF' is a mock prefix in patterns for testing + with patch("routes.bluetooth.TILE_PREFIXES", ["FF:FF"]): + result = detect_tracker("FF:FF:00:11:22:33", "Unknown") + assert result["type"] == "tile" + + +def test_detect_tracker_by_name(): + """Test tracker detection using name strings.""" + result = detect_tracker("00:11:22:33:44:55", "My AirTag") + assert result["type"] == "airtag" + assert result["risk"] == "high" + + +# --- Route Tests --- + + +def test_get_interfaces_route(client, mocker): + """Test the /interfaces endpoint with mocked system output.""" + mock_run = mocker.patch("subprocess.run") + # Mocking hciconfig output for a Linux system + mock_run.return_value = MagicMock( + stdout="hci0:\tType: Primary Bus: USB\n\tBD Address: 00:11:22:33:44:55 ACL MTU: 1021:8 SCO MTU: 64:1\n\tUP RUNNING\n" + ) + mocker.patch("platform.system", return_value="Linux") + mocker.patch("routes.bluetooth.check_tool", return_value=True) + + response = client.get("/bt/interfaces") + data = response.get_json() + + assert response.status_code == 200 + assert data["interfaces"][0]["name"] == "hci0" + assert data["interfaces"][0]["status"] == "up" + assert data["tools"]["hcitool"] is True + + +def test_stop_scan_route(client, mock_app_module): + """Test stopping a running scan process.""" + mock_process = MagicMock() + mock_app_module.bt_process = mock_process + + response = client.post("/bt/scan/stop") + + assert response.status_code == 200 + assert response.get_json()["status"] == "stopped" + mock_process.terminate.assert_called_once() + + +def test_enum_services_error_no_mac(client): + """Test service enumeration validation.""" + response = client.post("/bt/enum", json={}) + assert response.status_code == 200 + assert response.get_json()["status"] == "error" + + +def test_get_devices_route(client, mock_app_module): + """Test retrieving the current device list from memory.""" + mock_app_module.bt_devices = {"00:11:22:33:44:55": {"mac": "00:11:22:33:44:55", "name": "Test Device"}} + + response = client.get("/bt/devices") + data = response.get_json() + + assert response.status_code == 200 + assert len(data["devices"]) == 1 + assert data["devices"][0]["name"] == "Test Device" + + +def test_reload_oui_route(client, mocker): + """Test the OUI database reload functionality.""" + mocker.patch("routes.bluetooth.load_oui_database", return_value={"001122": "Test Corp"}) + + response = client.post("/bt/reload-oui") + data = response.get_json() + + assert response.status_code == 200 + assert data["status"] == "success" + assert data["entries"] > 0 +