113 lines
4.5 KiB
Python
113 lines
4.5 KiB
Python
|
|
"""Unit tests for the FIX-M20 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
|