Files
hermes-relay/plugin/tests/test_android_location.py

144 lines
5.3 KiB
Python

"""
Unit tests for ``plugin.tools.android_tool.android_location`` (Tier C1).
Uses only stdlib ``unittest`` + ``unittest.mock`` so it runs cleanly via::
python -m unittest plugin.tests.test_android_location
without pulling in the ``responses`` dependency that ``conftest.py`` imports
at pytest collection time. The conftest is pytest-only and is not loaded
by ``unittest``.
Coverage:
* happy path — bridge returns a full fix, tool echoes JSON.
* permission denied — bridge returns the helpful error body.
* stale location warning — bridge returns staleness_ms + warning field.
* schema registration sanity (name, parameters, handler wiring).
"""
from __future__ import annotations
import json
import sys
import unittest
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[2]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
# Import module-level so _post / _get patch targets resolve cleanly.
from plugin.tools import android_tool # noqa: E402
class TestAndroidLocationHappyPath(unittest.TestCase):
def test_returns_lat_lon_accuracy(self) -> None:
fake = {
"latitude": 37.4219,
"longitude": -122.0840,
"accuracy": 8.5,
"provider": "gps",
"timestamp": 1_700_000_000_000,
"staleness_ms": 12_000,
}
with mock.patch.object(android_tool, "_get", return_value=fake):
result = json.loads(android_tool.android_location())
self.assertEqual(result["latitude"], 37.4219)
self.assertEqual(result["longitude"], -122.0840)
self.assertEqual(result["accuracy"], 8.5)
self.assertEqual(result["provider"], "gps")
self.assertNotIn("error", result)
def test_includes_altitude_when_available(self) -> None:
fake = {
"latitude": 0.0,
"longitude": 0.0,
"accuracy": 10.0,
"altitude": 42.0,
"provider": "gps",
"timestamp": 0,
"staleness_ms": 0,
}
with mock.patch.object(android_tool, "_get", return_value=fake):
result = json.loads(android_tool.android_location())
self.assertEqual(result["altitude"], 42.0)
class TestAndroidLocationPermissionDenied(unittest.TestCase):
def test_permission_denied_error_passthrough(self) -> None:
"""Bridge returns a human-readable permission error; tool forwards it."""
fake = {
"error": (
"Grant location permission in Settings > Apps > "
"Hermes-Relay > Permissions"
)
}
with mock.patch.object(android_tool, "_get", return_value=fake):
result = json.loads(android_tool.android_location())
self.assertIn("error", result)
self.assertIn("location permission", result["error"])
def test_exception_becomes_error_json(self) -> None:
"""_get raises — tool returns {"error": "..."}."""
with mock.patch.object(
android_tool, "_get", side_effect=ConnectionError("refused")
):
result = json.loads(android_tool.android_location())
self.assertIn("error", result)
self.assertIn("refused", result["error"])
class TestAndroidLocationStaleFix(unittest.TestCase):
def test_stale_warning_field(self) -> None:
"""If the bridge reports a stale fix, the warning propagates."""
fake = {
"latitude": 37.0,
"longitude": -122.0,
"accuracy": 50.0,
"provider": "network",
"timestamp": 1_000_000,
"staleness_ms": 15 * 60 * 1000,
"warning": (
"Last known location is >5min old; open a maps app briefly "
"to refresh the fix."
),
}
with mock.patch.object(android_tool, "_get", return_value=fake):
result = json.loads(android_tool.android_location())
self.assertIn("warning", result)
self.assertGreater(result["staleness_ms"], 5 * 60 * 1000)
class TestAndroidLocationSchema(unittest.TestCase):
def test_registered_in_schemas(self) -> None:
self.assertIn("android_location", android_tool._SCHEMAS)
def test_registered_in_handlers(self) -> None:
self.assertIn("android_location", android_tool._HANDLERS)
def test_schema_has_required_fields(self) -> None:
schema = android_tool._SCHEMAS["android_location"]
self.assertEqual(schema["name"], "android_location")
self.assertIn("description", schema)
self.assertIn("sideload", schema["description"].lower())
self.assertIn("parameters", schema)
# No required args; multi-device relays may optionally target a device.
self.assertEqual(
set(schema["parameters"].get("properties", {})),
{"device"},
)
self.assertEqual(schema["parameters"].get("required", []), [])
def test_handler_dispatches_correctly(self) -> None:
fake = {"latitude": 1.0, "longitude": 2.0, "accuracy": 3.0}
with mock.patch.object(android_tool, "_get", return_value=fake):
handler = android_tool._HANDLERS["android_location"]
out = handler({})
result = json.loads(out)
self.assertEqual(result["latitude"], 1.0)
if __name__ == "__main__":
unittest.main()