Four new JSON routes mirror the Go /api/* handlers so the M5.4 parity tool can diff them: /api/version, /api/adults, /api/juniors, /api/payments. A small _unwrap_view_model_for_api() helper in app.py expands the three pre-serialised JSON strings in the view-model dicts and renames month_labels_json → month_labels and raw_payments_json → raw_payments to match the Go wire contract. Tests in test_app.py assert top-level key sets match the Go API schema and that member_data, month_labels, raw_payments are objects not strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
187 lines
7.9 KiB
Python
187 lines
7.9 KiB
Python
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,
|
|
}]
|
|
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,
|
|
}]
|
|
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',
|
|
}]
|
|
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()
|