106 lines
3.6 KiB
Python
106 lines
3.6 KiB
Python
"""Dashboard stats — pure-helper and RQ Score client contract tests."""
|
|
import asyncio
|
|
|
|
import httpx
|
|
|
|
from app.engine import stats as S
|
|
from app.engine.stats import clients
|
|
from app.engine.normalize import _posted_date
|
|
from app.db.repo import _activity_score
|
|
|
|
|
|
def test_posted_date_per_board():
|
|
assert _posted_date({"createdDate": "2026-06-18T10:37:17Z"}) == "2026-06-18T10:37:17Z" # Naukri
|
|
assert _posted_date({"postedDate": "2026-06-18T12:46:39Z"}) == "2026-06-18T12:46:39Z" # LinkedIn
|
|
assert _posted_date({"date_posted": "2026-06-17T17:38:14Z"}) == "2026-06-17T17:38:14Z" # Foundit
|
|
assert _posted_date({"jobDetails": {"createdDate": "2026-06-18T00:00:00Z"}}) is not None # nested
|
|
assert _posted_date({"nope": "x"}) is None
|
|
|
|
|
|
def test_match_scores_and_histogram():
|
|
opps = [{"match": {"score": 84}}, {"matchScore": 72}, {"match": {"score": 0}}, {}]
|
|
scores = S._match_scores(opps)
|
|
assert sorted(scores) == [72, 84] # 0 and missing dropped
|
|
h = S._histogram(scores)
|
|
assert sum(h) == 2 and len(h) == 11
|
|
|
|
|
|
def test_apply_window_freshness():
|
|
# all None when no posting dates (honest — never fabricated)
|
|
assert S._apply_window([{}, {"posted_date": None}])["urgency"] is None
|
|
w = S._apply_window([{"posted_date": "2026-06-19T00:00:00Z"}, {"posted_date": "2026-06-15T00:00:00Z"}])
|
|
assert w["dated"] == 2 and w["median_age_days"] is not None and w["urgency"] in ("high", "medium", "low")
|
|
|
|
|
|
def test_activity_score_weighting():
|
|
# applies weighted highest, then saves, views, searches
|
|
assert _activity_score(0, 0, 1, 0) > _activity_score(0, 1, 0, 0) > _activity_score(1, 0, 0, 0)
|
|
assert _activity_score(0, 0, 0, 0) == 0
|
|
|
|
|
|
class _Response:
|
|
status_code = 200
|
|
|
|
def __init__(self, payload):
|
|
self._payload = payload
|
|
|
|
def raise_for_status(self):
|
|
return None
|
|
|
|
def json(self):
|
|
return self._payload
|
|
|
|
|
|
def test_fetch_qx_reads_strict_rq_score_contract(monkeypatch):
|
|
async def _get(_self, _url, **_kwargs):
|
|
return _Response({"rq_score": 68.25, "quotients": {"technical": 71}})
|
|
|
|
monkeypatch.setattr(httpx.AsyncClient, "get", _get)
|
|
|
|
assert asyncio.run(clients.fetch_qx("user-uuid")) == {
|
|
"qx": 68.2,
|
|
"quotients": {"technical": 71},
|
|
}
|
|
|
|
|
|
def test_fetch_qx_does_not_alias_legacy_q_score(monkeypatch):
|
|
async def _get(_self, _url, **_kwargs):
|
|
return _Response({"q_score": 99, "quotients": {"technical": 99}})
|
|
|
|
monkeypatch.setattr(httpx.AsyncClient, "get", _get)
|
|
|
|
assert asyncio.run(clients.fetch_qx("user-uuid")) is None
|
|
|
|
|
|
def test_fetch_qx_treats_malformed_payload_as_absent(monkeypatch):
|
|
async def _get(_self, _url, **_kwargs):
|
|
return _Response([{"rq_score": 88}])
|
|
|
|
monkeypatch.setattr(httpx.AsyncClient, "get", _get)
|
|
|
|
assert asyncio.run(clients.fetch_qx("user-uuid")) is None
|
|
|
|
|
|
def test_fetch_qx_trend_ignores_malformed_and_absent_points(monkeypatch):
|
|
async def _get(_self, _url, **_kwargs):
|
|
return _Response({"points": [None, {"rq_score": "bad"}, {"rq_score": 35}, {"rq_score": 40}]})
|
|
|
|
monkeypatch.setattr(httpx.AsyncClient, "get", _get)
|
|
|
|
assert asyncio.run(clients.fetch_qx_trend("user-uuid")) == [35.0, 40.0]
|
|
|
|
|
|
def test_fetch_qx_trend_reads_strict_rq_score_points(monkeypatch):
|
|
async def _get(_self, _url, **_kwargs):
|
|
return _Response({
|
|
"points": [
|
|
{"rq_score": 35, "q_score": 91},
|
|
{"rq_score": 44.16, "q_score": 92},
|
|
{"q_score": 93},
|
|
]
|
|
})
|
|
|
|
monkeypatch.setattr(httpx.AsyncClient, "get", _get)
|
|
|
|
assert asyncio.run(clients.fetch_qx_trend("user-uuid")) == [35.0, 44.2]
|