All checks were successful
Deploy to K8s / deploy (push) Successful in 7s
Python's fetch_sheet_data read 9 sheet columns but skipped VS and Sync ID, causing make parity to report extra fields on every raw payment row emitted by the Go backend. Both columns are already on the sheet; add idx_vs / idx_sync_id lookups and the matching keys to the tx dict so the Python /api/* wire shape matches Go's RawTransaction. Update /api/* test fixtures to include vs/sync_id keys for realism. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
190 lines
8.0 KiB
Python
190 lines
8.0 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,
|
|
'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()
|