"""Unit tests for the FIX-M21 stale-IMEI recovery helpers. Covers: - ts_shared_rev.get_stale_imeis — the SQL selector - ingest_movement_rev.poll_stale_locations — the scheduler wrapper """ import sys import os import pytest from unittest.mock import MagicMock, patch from contextlib import contextmanager sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) os.environ.setdefault("TRACKSOLID_APP_KEY", "test_key") os.environ.setdefault("TRACKSOLID_APP_SECRET", "test_secret") os.environ.setdefault("TRACKSOLID_USER_ID", "test_user") os.environ.setdefault("TRACKSOLID_PWD_MD5", "test_md5") os.environ.setdefault("DATABASE_URL", "postgresql://test:test@localhost:5432/test") def _make_mock_conn(rows=None): """Cursor/connection double that returns `rows` from fetchall().""" mock_cur = MagicMock() mock_cur.fetchall.return_value = rows or [] mock_conn = MagicMock() mock_conn.cursor.return_value.__enter__ = lambda s: mock_cur mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False) return mock_conn, mock_cur @contextmanager def _conn_ctx(mock_conn): yield mock_conn class TestGetStaleImeis: def test_returns_devices_with_old_gps_time(self): from ts_shared_rev import get_stale_imeis mock_conn, mock_cur = _make_mock_conn(rows=[("imei_a",), ("imei_b",)]) with patch("ts_shared_rev.get_conn") as mock_get_conn: mock_get_conn.return_value = _conn_ctx(mock_conn) result = get_stale_imeis(stale_minutes=30) assert result == ["imei_a", "imei_b"] sql = str(mock_cur.execute.call_args) assert "enabled_flag = 1" in sql assert "INTERVAL" in sql.upper() or "interval" in sql assert "NULLS FIRST" in sql def test_returns_empty_when_no_stale(self): from ts_shared_rev import get_stale_imeis mock_conn, _ = _make_mock_conn(rows=[]) with patch("ts_shared_rev.get_conn") as mock_get_conn: mock_get_conn.return_value = _conn_ctx(mock_conn) assert get_stale_imeis() == [] class TestPollStaleLocations: def test_noop_when_empty(self): """If no IMEIs are stale, get_device_locations must NOT be called.""" import ingest_movement_rev as mod with patch.object(mod, "get_stale_imeis", return_value=[]), \ patch.object(mod, "get_device_locations") as mock_refresh: mod.poll_stale_locations() mock_refresh.assert_not_called() def test_invokes_refresh_with_stale_list(self): """When stale IMEIs are present, get_device_locations is called once with the exact list returned by get_stale_imeis.""" import ingest_movement_rev as mod stale = ["imei_a", "imei_b", "imei_c"] with patch.object(mod, "get_stale_imeis", return_value=stale), \ patch.object(mod, "get_device_locations") as mock_refresh: mod.poll_stale_locations() mock_refresh.assert_called_once_with(stale) class TestUpsertLivePositionGuards: """Spot-checks on the time-guard contract — covers what the dashboard relies on (no rewinding the marker on stale alarm arrivals).""" def test_skips_invalid_fix(self): from ts_shared_rev import upsert_live_position mock_cur = MagicMock() # Zero-Island assert upsert_live_position(mock_cur, "imei_x", 0, 0, "2026-05-21 10:00:00") == 0 mock_cur.execute.assert_not_called() def test_skips_missing_gps_time(self): from ts_shared_rev import upsert_live_position mock_cur = MagicMock() assert upsert_live_position(mock_cur, "imei_x", -1.29, 36.82, None) == 0 mock_cur.execute.assert_not_called() def test_skips_missing_imei(self): from ts_shared_rev import upsert_live_position mock_cur = MagicMock() assert upsert_live_position(mock_cur, None, -1.29, 36.82, "2026-05-21 10:00:00") == 0 mock_cur.execute.assert_not_called() def test_executes_upsert_for_valid_fix(self): from ts_shared_rev import upsert_live_position mock_cur = MagicMock() mock_cur.rowcount = 1 n = upsert_live_position(mock_cur, "imei_x", -1.29, 36.82, "2026-05-21 10:00:00", speed=42.5) assert n == 1 mock_cur.execute.assert_called_once() sql = str(mock_cur.execute.call_args) # The time-guard is the load-bearing detail — verify it's present. assert "EXCLUDED.gps_time > tracksolid.live_positions.gps_time" in sql