import unittest import json from unittest.mock import patch from app import app EXPECTED_ADULTS_KEYS = { "months", "raw_months", "results", "totals", "member_data", "month_labels", "raw_payments", "credits", "debts", "unmatched", "attendance_url", "payments_url", "bank_account", "current_month", } EXPECTED_JUNIORS_KEYS = EXPECTED_ADULTS_KEYS EXPECTED_PAYMENTS_KEYS = {"grouped_payments", "sorted_people", "attendance_url", "payments_url"} EXPECTED_VERSION_KEYS = {"tag", "commit", "build_date"} def _bypass_cache(cache_key, sheet_id, fetch_func, *args, serialize=None, deserialize=None, **kwargs): """Test helper: call fetch_func directly, bypassing the cache layer.""" return fetch_func(*args, **kwargs) class TestWebApp(unittest.TestCase): def setUp(self): app.config['TESTING'] = True self.client = app.test_client() def test_index_page(self): """Test that / returns the refresh meta tag""" response = self.client.get('/') self.assertEqual(response.status_code, 200) self.assertIn(b'url=/adults', response.data) @patch('app.get_cached_data', side_effect=_bypass_cache) @patch('app.fetch_sheet_data') def test_payments_route(self, mock_fetch_sheet, mock_cache): """Test that /payments returns 200 and groups transactions""" mock_fetch_sheet.return_value = [{ 'date': '2026-01-01', 'amount': 750, 'person': 'Test Member', 'purpose': '2026-01', 'message': 'Direct Member Payment', 'sender': 'External Bank User' }] response = self.client.get('/payments') self.assertEqual(response.status_code, 200) self.assertIn(b'Payments Ledger', response.data) self.assertIn(b'Test Member', response.data) self.assertIn(b'Direct Member Payment', response.data) @patch('app.get_cached_data', side_effect=_bypass_cache) @patch('app.fetch_sheet_data') @patch('app.fetch_exceptions', return_value={}) @patch('app.get_members_with_fees') def test_adults_route(self, mock_get_members, mock_exceptions, mock_fetch_sheet, mock_cache): """Test that /adults returns 200 and shows combined matches""" mock_get_members.return_value = ( [('Test Member', 'A', {'2026-01': (750, 4)})], ['2026-01'] ) mock_fetch_sheet.return_value = [{ 'date': '2026-01-01', 'amount': 750, 'person': 'Test Member', 'purpose': '2026-01', 'message': 'test payment', 'sender': 'External Bank User', 'inferred_amount': 750 }] response = self.client.get('/adults') self.assertEqual(response.status_code, 200) self.assertIn(b'Adults Dashboard', response.data) self.assertIn(b'Test Member', response.data) self.assertNotIn(b'OK', response.data) self.assertIn(b'750/750 CZK (4)', response.data) @patch('app.get_cached_data', side_effect=_bypass_cache) @patch('app.fetch_sheet_data') @patch('app.fetch_exceptions', return_value={}) @patch('app.get_junior_members_with_fees') def test_juniors_route(self, mock_get_junior_members, mock_exceptions, mock_fetch_sheet, mock_cache): """Test that /juniors returns 200, uses single line format, and displays '?' properly""" mock_get_junior_members.return_value = ( [ ('Junior One', 'J', {'2026-01': (500, 3, 0, 3)}), ('Junior Two', 'X', {'2026-01': ('?', 1, 0, 1)}) ], ['2026-01'] ) mock_exceptions.return_value = {} mock_fetch_sheet.return_value = [{ 'date': '2026-01-15', 'amount': 500, 'person': 'Junior One', 'purpose': '2026-01', 'message': '', 'sender': 'Parent', 'inferred_amount': 500 }] response = self.client.get('/juniors') self.assertEqual(response.status_code, 200) self.assertIn(b'Juniors Dashboard', response.data) self.assertIn(b'Junior One', response.data) self.assertIn(b'Junior Two', response.data) self.assertNotIn(b'OK', response.data) self.assertIn(b'500/500 CZK', response.data) self.assertIn(b'?', response.data) def test_api_version(self): """Test /api/version returns BUILD_META keys as JSON.""" response = self.client.get('/api/version') self.assertEqual(response.status_code, 200) self.assertTrue(response.is_json) data = json.loads(response.data) self.assertEqual(set(data.keys()), EXPECTED_VERSION_KEYS) @patch('app.get_cached_data', side_effect=_bypass_cache) @patch('app.fetch_sheet_data') @patch('app.fetch_exceptions', return_value={}) @patch('app.get_members_with_fees') def test_api_adults(self, mock_get_members, mock_exceptions, mock_fetch_sheet, mock_cache): """Test /api/adults returns JSON with correct top-level keys and unwrapped fields.""" mock_get_members.return_value = ( [('Test Member', 'A', {'2026-01': (750, 4)})], ['2026-01'] ) mock_fetch_sheet.return_value = [{ 'date': '2026-01-01', 'amount': 750, 'person': 'Test Member', 'purpose': '2026-01', 'message': 'test payment', 'sender': 'External Bank User', 'inferred_amount': 750, 'vs': '', 'sync_id': 'abc123', }] response = self.client.get('/api/adults') self.assertEqual(response.status_code, 200) self.assertTrue(response.is_json) data = json.loads(response.data) self.assertEqual(set(data.keys()), EXPECTED_ADULTS_KEYS) self.assertIsInstance(data['member_data'], dict) self.assertIsInstance(data['month_labels'], dict) self.assertIsInstance(data['raw_payments'], dict) @patch('app.get_cached_data', side_effect=_bypass_cache) @patch('app.fetch_sheet_data') @patch('app.fetch_exceptions', return_value={}) @patch('app.get_junior_members_with_fees') def test_api_juniors(self, mock_get_junior_members, mock_exceptions, mock_fetch_sheet, mock_cache): """Test /api/juniors returns JSON with correct top-level keys and unwrapped fields.""" mock_get_junior_members.return_value = ( [ ('Junior One', 'J', {'2026-01': (500, 3, 0, 3)}), ('Junior Two', 'X', {'2026-01': ('?', 1, 0, 1)}), ], ['2026-01'] ) mock_fetch_sheet.return_value = [{ 'date': '2026-01-15', 'amount': 500, 'person': 'Junior One', 'purpose': '2026-01', 'message': '', 'sender': 'Parent', 'inferred_amount': 500, 'vs': '', 'sync_id': 'def456', }] response = self.client.get('/api/juniors') self.assertEqual(response.status_code, 200) self.assertTrue(response.is_json) data = json.loads(response.data) self.assertEqual(set(data.keys()), EXPECTED_JUNIORS_KEYS) self.assertIsInstance(data['member_data'], dict) self.assertIsInstance(data['month_labels'], dict) self.assertIsInstance(data['raw_payments'], dict) @patch('app.get_cached_data', side_effect=_bypass_cache) @patch('app.fetch_sheet_data') def test_api_payments(self, mock_fetch_sheet, mock_cache): """Test /api/payments returns JSON with correct top-level keys.""" mock_fetch_sheet.return_value = [{ 'date': '2026-01-01', 'amount': 750, 'person': 'Test Member', 'purpose': '2026-01', 'message': 'test', 'sender': 'Someone', 'vs': '', 'sync_id': 'ghi789', }] response = self.client.get('/api/payments') self.assertEqual(response.status_code, 200) self.assertTrue(response.is_json) data = json.loads(response.data) self.assertEqual(set(data.keys()), EXPECTED_PAYMENTS_KEYS) self.assertIsInstance(data['grouped_payments'], dict) self.assertIsInstance(data['sorted_people'], list) if __name__ == '__main__': unittest.main()