feat(py): M5.3 — add Python /api/* shadow endpoints
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>
This commit is contained in:
@@ -1,7 +1,17 @@
|
||||
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."""
|
||||
@@ -97,5 +107,80 @@ class TestWebApp(unittest.TestCase):
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user