import sys import os import json from unittest.mock import MagicMock, patch # Add project root to path sys.path.append(os.getcwd()) # Mock low-level deps BEFORE importing the target module. # logger_reset must be a passthrough — if it stays a MagicMock the decorator # replaces the decorated function with a MagicMock and tests get garbage results. mock_lib_general = MagicMock() mock_lib_general.logger_reset = lambda f: f sys.modules['app.config'] = MagicMock() sys.modules['app.lib_general'] = mock_lib_general sys.modules['app.db_sql'] = MagicMock() sys.modules['app.lib_redis_helpers'] = MagicMock() from app.methods import idaa_novi_verify_methods as m # ── Helpers ─────────────────────────────────────────────────────────────── def _make_cfg(): return { 'novi_api_root_url': 'https://www.idaa.org/api', 'novi_idaa_api_key': 'dGVzdGtleQ==', } def _novi_resp(email='alice@idaa.org', first='Alice', last='Smith', name=None): d = {'Email': email, 'FirstName': first, 'LastName': last} if name is not None: d['Name'] = name return d def _set_redis(cached_value=None): """Set redis_client on the already-imported module's imported name.""" r = MagicMock() r.get.return_value = cached_value sys.modules['app.lib_redis_helpers'].redis_client = r return r # ── Cache hit bypasses Novi ─────────────────────────────────────────────── def test_cache_hit_bypasses_novi(): print('--- test_cache_hit_bypasses_novi ---') cached = json.dumps({'status': 200, 'verified': True, 'full_name': 'Bob J.', 'email': 'bob@idaa.org'}) redis_mock = _set_redis(cached_value=cached) with patch.object(m, '_load_idaa_cfg', return_value=_make_cfg()), \ patch('requests.get') as mock_get: result = m.verify_novi_member('some-uuid') print('Result:', result) assert result['status'] == 200 assert result['full_name'] == 'Bob J.' mock_get.assert_not_called() # Novi was never contacted print('PASS') # ── Verified 200 ────────────────────────────────────────────────────────── def test_verified_member_200(): print('--- test_verified_member_200 ---') mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = _novi_resp() redis_mock = _set_redis() with patch.object(m, '_load_idaa_cfg', return_value=_make_cfg()), \ patch('requests.get', return_value=mock_resp): result = m.verify_novi_member('abc-123') print('Result:', result) assert result['status'] == 200 assert result['verified'] is True assert result['full_name'] == 'Alice S.' assert result['email'] == 'alice@idaa.org' redis_mock.setex.assert_called_once() # verified result cached print('PASS') # ── Email normalization: space → + ──────────────────────────────────────── def test_email_space_normalization(): print('--- test_email_space_normalization ---') mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = _novi_resp(email='alice member@idaa.org') _set_redis() with patch.object(m, '_load_idaa_cfg', return_value=_make_cfg()), \ patch('requests.get', return_value=mock_resp): result = m.verify_novi_member('abc-123') print('Result:', result) assert result['status'] == 200 assert result['email'] == 'alice+member@idaa.org' print('PASS') # ── Display name format ─────────────────────────────────────────────────── def test_display_name_format(): print('--- test_display_name_format ---') cases = [ (_novi_resp(first='Alice', last='Smith'), 'Alice S.'), (_novi_resp(first='Alice', last=''), 'Alice'), (_novi_resp(first='', last='Smith', name='Dr. Alice'), 'Dr. Alice'), (_novi_resp(first='', last='', name='Dr. Alice'), 'Dr. Alice'), (_novi_resp(first='', last='', name=''), 'Member'), ] for novi_data, expected_name in cases: mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = novi_data _set_redis() with patch.object(m, '_load_idaa_cfg', return_value=_make_cfg()), \ patch('requests.get', return_value=mock_resp): result = m.verify_novi_member('abc-123') assert result['status'] == 200 assert result['full_name'] == expected_name, \ f"Expected '{expected_name}', got '{result['full_name']}' for input {novi_data}" print('All display name cases PASS') # ── Empty-member anti-pattern: Novi 200, no Email ───────────────────────── def test_empty_member_returns_404(): print('--- test_empty_member_returns_404 ---') mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json.return_value = {} # Novi 200 with no identity data redis_mock = _set_redis() with patch.object(m, '_load_idaa_cfg', return_value=_make_cfg()), \ patch('requests.get', return_value=mock_resp): result = m.verify_novi_member('ghost-uuid') print('Result:', result) assert result['status'] == 404 redis_mock.setex.assert_not_called() # 404 must NOT be cached print('PASS') # ── Novi 404 ────────────────────────────────────────────────────────────── def test_novi_404_returns_404(): print('--- test_novi_404_returns_404 ---') mock_resp = MagicMock() mock_resp.status_code = 404 redis_mock = _set_redis() with patch.object(m, '_load_idaa_cfg', return_value=_make_cfg()), \ patch('requests.get', return_value=mock_resp): result = m.verify_novi_member('missing-uuid') print('Result:', result) assert result['status'] == 404 redis_mock.setex.assert_not_called() print('PASS') # ── Novi 429 ────────────────────────────────────────────────────────────── def test_novi_429_returns_429(): print('--- test_novi_429_returns_429 ---') mock_resp = MagicMock() mock_resp.status_code = 429 redis_mock = _set_redis() with patch.object(m, '_load_idaa_cfg', return_value=_make_cfg()), \ patch('requests.get', return_value=mock_resp): result = m.verify_novi_member('any-uuid') print('Result:', result) assert result['status'] == 429 redis_mock.setex.assert_not_called() print('PASS') # ── Novi 5xx → 503 ──────────────────────────────────────────────────────── def test_novi_5xx_returns_503(): print('--- test_novi_5xx_returns_503 ---') mock_resp = MagicMock() mock_resp.status_code = 502 _set_redis() with patch.object(m, '_load_idaa_cfg', return_value=_make_cfg()), \ patch('requests.get', return_value=mock_resp): result = m.verify_novi_member('any-uuid') print('Result:', result) assert result['status'] == 503 print('PASS') # ── Novi unreachable → 503 ──────────────────────────────────────────────── def test_novi_unreachable_returns_503(): print('--- test_novi_unreachable_returns_503 ---') import requests as req_lib _set_redis() with patch.object(m, '_load_idaa_cfg', return_value=_make_cfg()), \ patch('requests.get', side_effect=req_lib.exceptions.ConnectionError('refused')): result = m.verify_novi_member('any-uuid') print('Result:', result) assert result['status'] == 503 print('PASS')