Files
fuj-management/legacy/tests/test_app.py
T
kacerrandClaude Opus 5.5 62c6dc2ca0 fix(sync): dashboard sync covers current year plus Nov-Dec of previous
/sync-bank now uses one shared default window in both implementations:
1 November of the previous year to 31 December of the current year.
Payments dated around New Year are picked up in January; duplicates
are still filtered by Sync ID.

Python used a hardcoded 2026 window and would have stopped syncing on
2027-01-01; Go used the current calendar year only.

Adds make sync-year [YEAR=N] with the same window. The CLI default
without flags (last 30 days) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 12:04:59 +02:00

206 lines
8.8 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", "months_to_show",
}
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()
@patch('app.flush_cache', return_value=0)
@patch('app.infer_payments')
@patch('app.sync_to_sheets')
def test_sync_bank_uses_default_window(self, mock_sync, mock_infer, mock_flush):
"""/sync-bank syncs the current year plus Nov-Dec of the previous one, not a hardcoded year."""
from datetime import datetime
year = datetime.now().year
response = self.client.get('/sync-bank')
self.assertEqual(response.status_code, 200)
kwargs = mock_sync.call_args.kwargs
self.assertEqual(kwargs['date_from_str'], f"{year - 1}-11-01")
self.assertEqual(kwargs['date_to_str'], f"{year}-12-31")
mock_infer.assert_called_once()
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)
self.assertIsInstance(data['months_to_show'], int)
@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)
self.assertIsInstance(data['months_to_show'], int)
@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()