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:
2026-05-07 22:37:14 +02:00
parent 68810369bd
commit 40e4a9e45e
2 changed files with 165 additions and 1 deletions

View File

@@ -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()