Files
fuj-management/tests/test_app.py
Jan Novak 033349cafa
All checks were successful
Deploy to K8s / deploy (push) Successful in 13s
Build and Push / build (push) Successful in 32s
refactor: code quality improvements across the backend
- Remove insecure SSL verification bypass in attendance.py
- Add gunicorn as production WSGI server (Dockerfile + entrypoint)
- Fix silent data loss in reconciliation (log + surface unmatched members)
- Add required column validation in payment sheet parsing
- Add input validation on /qr route (account format, amount bounds, SPD injection)
- Centralize configuration into scripts/config.py
- Extract credentials path to env-configurable constant
- Hide unmatched transactions from reconcile-juniors page
- Fix test mocks to bypass cache layer (all 8 tests now pass reliably)
- Add pytest + pytest-cov dev dependencies
- Fix typo "Inffering" in infer_payments.py
- Update CLAUDE.md to reflect current project state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 11:40:32 +01:00

135 lines
5.3 KiB
Python

import unittest
from unittest.mock import patch
from app import app
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=/fees', response.data)
@patch('app.get_cached_data', side_effect=_bypass_cache)
@patch('app.get_members_with_fees')
@patch('app.fetch_exceptions', return_value={})
def test_fees_route(self, mock_exceptions, mock_get_members, mock_cache):
"""Test that /fees returns 200 and renders the dashboard"""
mock_get_members.return_value = (
[('Test Member', 'A', {'2026-01': (750, 4)})],
['2026-01']
)
response = self.client.get('/fees')
self.assertEqual(response.status_code, 200)
self.assertIn(b'FUJ Fees Dashboard', response.data)
self.assertIn(b'Test Member', response.data)
@patch('app.get_cached_data', side_effect=_bypass_cache)
@patch('app.get_junior_members_with_fees')
@patch('app.fetch_exceptions', return_value={})
def test_fees_juniors_route(self, mock_exceptions, mock_get_junior_members, mock_cache):
"""Test that /fees-juniors returns 200 and renders the junior dashboard"""
mock_get_junior_members.return_value = (
[
('Test Junior 1', 'J', {'2026-01': ('?', 1, 0, 1)}),
('Test Junior 2', 'J', {'2026-01': (500, 4, 1, 3)})
],
['2026-01']
)
response = self.client.get('/fees-juniors')
self.assertEqual(response.status_code, 200)
self.assertIn(b'FUJ Junior Fees Dashboard', response.data)
self.assertIn(b'Test Junior 1', response.data)
self.assertIn(b'? / 1 (J)', response.data)
self.assertIn(b'500 CZK / 4 (1A+3J)', 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_reconcile_route(self, mock_get_members, mock_exceptions, mock_fetch_sheet, mock_cache):
"""Test that /reconcile returns 200 and shows 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('/reconcile')
self.assertEqual(response.status_code, 200)
self.assertIn(b'Payment Reconciliation', response.data)
self.assertIn(b'Test Member', response.data)
self.assertIn(b'OK', 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')
@patch('app.get_junior_members_with_fees')
def test_reconcile_juniors_route(self, mock_get_junior, mock_exceptions, mock_transactions, mock_cache):
"""Test that /reconcile-juniors correctly computes balances for juniors."""
mock_get_junior.return_value = (
[
('Junior One', 'J', {'2026-01': (500, 4, 2, 2)}),
('Junior Two', 'X', {'2026-01': ('?', 1, 0, 1)})
],
['2026-01']
)
mock_exceptions.return_value = {}
mock_transactions.return_value = [{
'date': '2026-01-15',
'amount': 500,
'person': 'Junior One',
'purpose': '2026-01',
'message': '',
'sender': 'Parent',
'inferred_amount': 500
}]
response = self.client.get('/reconcile-juniors')
self.assertEqual(response.status_code, 200)
self.assertIn(b'Junior Payment Reconciliation', response.data)
self.assertIn(b'Junior One', response.data)
self.assertIn(b'Junior Two', response.data)
self.assertIn(b'OK', response.data)
self.assertIn(b'?', response.data)
if __name__ == '__main__':
unittest.main()