first commit

This commit is contained in:
Mateusz Gruszczyński
2026-03-13 15:17:32 +01:00
commit 986ffb200a
91 changed files with 4423 additions and 0 deletions

49
tests/conftest.py Normal file
View File

@@ -0,0 +1,49 @@
import pytest
from app import create_app
from app.config import TestConfig
from app.extensions import db
from app.models import User, seed_categories, seed_default_settings
@pytest.fixture
def app():
app = create_app(TestConfig)
with app.app_context():
db.drop_all()
db.create_all()
seed_categories()
seed_default_settings()
admin = User(email='admin@test.com', full_name='Admin', role='admin', must_change_password=False, language='pl')
admin.set_password('Password123!')
user = User(email='user@test.com', full_name='User', role='user', must_change_password=False, language='pl')
user.set_password('Password123!')
db.session.add_all([admin, user])
db.session.commit()
yield app
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def runner(app):
return app.test_cli_runner()
def login(client, email='user@test.com', password='Password123!'):
return client.post('/login', data={'email': email, 'password': password}, follow_redirects=True)
@pytest.fixture
def logged_user(client):
login(client)
return client
@pytest.fixture
def logged_admin(client):
login(client, 'admin@test.com')
return client

56
tests/test_admin.py Normal file
View File

@@ -0,0 +1,56 @@
from app.models import AppSetting, Category, User
def test_admin_can_create_category(logged_admin, app):
response = logged_admin.post('/admin/categories', data={'key': 'pets', 'name_pl': 'Zwierzęta', 'name_en': 'Pets', 'color': 'info', 'is_active': 'y'}, follow_redirects=True)
assert response.status_code == 200
with app.app_context():
assert Category.query.filter_by(key='pets').first() is not None
def test_non_admin_forbidden(logged_user):
response = logged_user.get('/admin/')
assert response.status_code == 403
def test_admin_can_create_user(logged_admin, app):
response = logged_admin.post('/admin/users', data={
'full_name': 'New User',
'email': 'new@example.com',
'role': 'user',
'language': 'en',
'must_change_password': 'y'
}, follow_redirects=True)
assert response.status_code == 200
with app.app_context():
assert User.query.filter_by(email='new@example.com').first() is not None
def test_admin_can_update_settings_and_flags(logged_admin, app):
response = logged_admin.post('/admin/settings', data={
'registration_enabled': 'on',
'max_upload_mb': '12',
'smtp_host': 'smtp.example.com',
'smtp_port': '587',
'smtp_username': 'mailer',
'smtp_password': 'secret',
'smtp_sender': 'noreply@example.com',
'smtp_use_tls': 'on',
'company_name': 'Test Co'
}, follow_redirects=True)
assert response.status_code == 200
with app.app_context():
assert AppSetting.get('registration_enabled') == 'true'
user = User.query.filter_by(email='user@test.com').first()
user_id = user.id
response = logged_admin.post(f'/admin/users/{user_id}/toggle-password-change', follow_redirects=True)
assert response.status_code == 200
with app.app_context():
user = User.query.filter_by(email='user@test.com').first()
assert user.must_change_password is True
def test_admin_dashboard_system_info(logged_admin):
response = logged_admin.get('/admin/')
assert response.status_code == 200
assert b'Engine:' in response.data

View File

@@ -0,0 +1,5 @@
def test_admin_audit_page(logged_admin):
response = logged_admin.get('/admin/audit')
assert response.status_code == 200
assert b'clipboard' in response.data or b'Audit' in response.data or b'Operacje' in response.data

24
tests/test_auth.py Normal file
View File

@@ -0,0 +1,24 @@
from app.models import PasswordResetToken, User
def test_login_success(client):
response = client.post('/login', data={'email': 'user@test.com', 'password': 'Password123!'}, follow_redirects=True)
assert response.status_code == 200
assert b'Dashboard' in response.data or b'Panel' in response.data
def test_honeypot_blocks_login(client):
response = client.post('/login', data={'email': 'user@test.com', 'password': 'Password123!', 'website': 'spam'}, follow_redirects=True)
assert response.status_code == 200
def test_password_reset_flow(client, app):
client.post('/forgot-password', data={'email': 'user@test.com'}, follow_redirects=True)
with app.app_context():
token = PasswordResetToken.query.join(User).filter(User.email == 'user@test.com').first()
assert token is not None
response = client.post(f'/reset-password/{token.token}', data={'password': 'NewPassword123!', 'confirm_password': 'NewPassword123!'}, follow_redirects=True)
assert response.status_code == 200
with app.app_context():
user = User.query.filter_by(email='user@test.com').first()
assert user.check_password('NewPassword123!')

32
tests/test_errors.py Normal file
View File

@@ -0,0 +1,32 @@
from io import BytesIO
from app.extensions import db
from app.models import AppSetting
def test_not_found(client):
response = client.get('/missing-page')
assert response.status_code == 404
def test_json_404(client):
response = client.get('/missing-page', headers={'Accept': 'application/json'})
assert response.status_code == 404
assert response.is_json
assert response.get_json()['status_code'] == 404
def test_large_upload_returns_413(logged_user, app):
with app.app_context():
AppSetting.set('max_upload_mb', '1')
db.session.commit()
data = {
'title': 'Huge file',
'amount': '10.00',
'currency': 'PLN',
'purchase_date': '2026-03-10',
'payment_method': 'card',
'document': (BytesIO(b'x' * (2 * 1024 * 1024)), 'big.jpg'),
}
response = logged_user.post('/expenses/create', data=data, content_type='multipart/form-data')
assert response.status_code == 413

110
tests/test_expenses.py Normal file
View File

@@ -0,0 +1,110 @@
from datetime import date
from app.extensions import db
from app.models import Budget, Category, Expense, User
def test_create_manual_expense(logged_user, app):
with app.app_context():
category = Category.query.first()
category_id = category.id
response = logged_user.post('/expenses/create', data={
'title': 'Milk',
'vendor': 'Store',
'description': '2L milk',
'amount': '12.50',
'currency': 'PLN',
'purchase_date': '2026-03-10',
'payment_method': 'card',
'category_id': str(category_id),
'recurring_period': 'none',
'status': 'confirmed',
}, follow_redirects=True)
assert response.status_code == 200
with app.app_context():
expense = Expense.query.filter_by(title='Milk').first()
assert expense is not None
assert expense.vendor == 'Store'
def test_expense_list_and_exports(logged_user, app):
with app.app_context():
user = User.query.filter_by(email='user@test.com').first()
expense = Expense(user_id=user.id, title='Bread', amount=4.99, currency='PLN', purchase_date=date(2026, 3, 1), payment_method='cash')
db.session.add(expense)
db.session.commit()
response = logged_user.get('/expenses/?year=2026&month=3')
assert b'Bread' in response.data
csv_response = logged_user.get('/expenses/export.csv')
assert csv_response.status_code == 200
assert b'Bread' in csv_response.data
pdf_response = logged_user.get('/expenses/export.pdf')
assert pdf_response.status_code == 200
assert pdf_response.mimetype == 'application/pdf'
def test_edit_delete_and_budget(logged_user, app):
with app.app_context():
user = User.query.filter_by(email='user@test.com').first()
category = Category.query.first()
category_id = category.id
expense = Expense(user_id=user.id, title='Taxi', amount=20, currency='PLN', purchase_date=date(2026, 3, 4), payment_method='cash', category_id=category_id)
db.session.add(expense)
db.session.commit()
expense_id = expense.id
response = logged_user.post(f'/expenses/{expense_id}/edit', data={
'title': 'Taxi Updated',
'vendor': 'Bolt',
'description': 'Airport ride',
'amount': '25.00',
'currency': 'PLN',
'purchase_date': '2026-03-04',
'payment_method': 'card',
'category_id': str(category_id),
'recurring_period': 'monthly',
'status': 'confirmed',
}, follow_redirects=True)
assert response.status_code == 200
with app.app_context():
updated = db.session.get(Expense, expense_id)
assert updated.title == 'Taxi Updated'
assert updated.vendor == 'Bolt'
response = logged_user.post('/expenses/budgets', data={'category_id': str(category_id), 'year': '2026', 'month': '3', 'amount': '300', 'alert_percent': '80'}, follow_redirects=True)
assert response.status_code == 200
with app.app_context():
assert Budget.query.filter_by(year=2026, month=3).first() is not None
response = logged_user.post(f'/expenses/{expense_id}/delete', follow_redirects=True)
assert response.status_code == 200
with app.app_context():
assert db.session.get(Expense, expense_id).is_deleted is True
def test_expense_list_filters_sort_and_grouping(logged_user, app):
with app.app_context():
user = User.query.filter_by(email='user@test.com').first()
category = Category.query.first()
other = Category.query.filter(Category.id != category.id).first()
db.session.add_all([
Expense(user_id=user.id, title='Alpha', vendor='Shop A', amount=10, currency='PLN', purchase_date=date(2026, 3, 5), payment_method='card', category_id=category.id, status='confirmed'),
Expense(user_id=user.id, title='Zulu', vendor='Shop B', amount=99, currency='PLN', purchase_date=date(2026, 3, 6), payment_method='cash', category_id=other.id if other else category.id, status='new'),
])
db.session.commit()
response = logged_user.get('/expenses/?year=2026&month=3&q=Zulu&sort_by=amount&sort_dir=desc&group_by=category')
assert response.status_code == 200
assert b'Zulu' in response.data
assert b'Alpha' not in response.data
assert b'Filtered total' in response.data or 'Suma po filtrach'.encode() in response.data
def test_expense_export_respects_status_filter(logged_user, app):
with app.app_context():
user = User.query.filter_by(email='user@test.com').first()
db.session.add_all([
Expense(user_id=user.id, title='Confirmed expense', amount=11, currency='PLN', purchase_date=date(2026, 3, 7), payment_method='card', status='confirmed'),
Expense(user_id=user.id, title='Needs review expense', amount=22, currency='PLN', purchase_date=date(2026, 3, 8), payment_method='card', status='needs_review'),
])
db.session.commit()
csv_response = logged_user.get('/expenses/export.csv?year=2026&month=3&status=confirmed')
assert csv_response.status_code == 200
assert b'Confirmed expense' in csv_response.data
assert b'Needs review expense' not in csv_response.data

View File

@@ -0,0 +1,89 @@
from datetime import date
from io import BytesIO
from PIL import Image
from app.extensions import db
from app.models import AppSetting, Category, DocumentAttachment, Expense, User
from tests.conftest import login
def _png_bytes(color='red'):
image = Image.new('RGB', (32, 32), color=color)
buffer = BytesIO()
image.save(buffer, format='PNG')
buffer.seek(0)
return buffer
def test_manifest_and_service_worker(client):
manifest = client.get('/manifest.json')
assert manifest.status_code == 200
assert manifest.get_json()['display'] == 'standalone'
sw = client.get('/service-worker.js')
assert sw.status_code == 200
assert b'skipWaiting' in sw.data
def test_webhook_creates_expense(client, app):
with app.app_context():
AppSetting.set('webhook_api_token', 'secret123')
db.session.commit()
category = Category.query.first()
response = client.post(
'/api/webhooks/expenses',
json={
'user_email': 'user@test.com',
'title': 'Webhook Lunch',
'amount': '25.50',
'currency': 'PLN',
'purchase_date': '2026-03-10',
'category_key': category.key,
},
headers={'X-Webhook-Token': 'secret123'},
)
assert response.status_code == 200
with app.app_context():
expense = Expense.query.filter_by(title='Webhook Lunch').first()
assert expense is not None
assert float(expense.amount) == 25.5
def test_multiple_attachments_saved(logged_user, app):
with app.app_context():
category = Category.query.first()
data = {
'title': 'Multi doc expense',
'amount': '12.34',
'purchase_date': '2026-03-12',
'category_id': str(category.id),
'payment_method': 'card',
'vendor': 'Vendor',
'description': 'Desc',
'currency': 'PLN',
'tags': 'tag1',
'recurring_period': 'none',
'status': 'confirmed',
'rotate': '0',
'scale_percent': '100',
'document': [(_png_bytes('red'), 'one.png'), (_png_bytes('blue'), 'two.png')],
}
response = logged_user.post('/expenses/create', data=data, content_type='multipart/form-data', follow_redirects=True)
assert response.status_code == 200
with app.app_context():
expense = Expense.query.filter_by(vendor='Vendor').order_by(Expense.id.desc()).first()
assert expense is not None
assert expense.title == 'Multi doc expense'
assert len(expense.attachments) == 2
assert DocumentAttachment.query.count() >= 2
def test_manual_report_run_from_admin(logged_admin, app):
with app.app_context():
user = User.query.filter_by(email='user@test.com').first()
user.report_frequency = 'monthly'
db.session.add(Expense(user_id=user.id, title='Report item', amount=9, currency='PLN', purchase_date=date.today(), payment_method='card'))
db.session.commit()
response = logged_admin.post('/admin/run-reports', follow_redirects=True)
assert response.status_code == 200
assert b'Queued/sent reports' in response.data

40
tests/test_main.py Normal file
View File

@@ -0,0 +1,40 @@
from datetime import date
from app.extensions import db
from app.models import Expense, User
def test_dashboard_and_analytics(logged_user, app):
with app.app_context():
user = User.query.filter_by(email='user@test.com').first()
db.session.add(Expense(user_id=user.id, title='Fuel', amount=100, currency='PLN', purchase_date=date(2026, 2, 5), payment_method='card'))
db.session.add(Expense(user_id=user.id, title='Fuel', amount=50, currency='PLN', purchase_date=date(2026, 3, 5), payment_method='card'))
db.session.commit()
response = logged_user.get('/dashboard?year=2026&month=3')
assert response.status_code == 200
api = logged_user.get('/analytics/data?year=2026')
assert api.status_code == 200
data = api.get_json()
assert any(item['month'] == 2 for item in data['yearly_totals'])
assert 'category_totals' in data
def test_preferences_language_switch_and_statistics(logged_user, app):
response = logged_user.post('/preferences', data={'language': 'en', 'theme': 'dark', 'report_frequency': 'monthly', 'default_currency': 'EUR'}, follow_redirects=True)
assert response.status_code == 200
stats = logged_user.get('/statistics?year=2026')
assert stats.status_code == 200
with app.app_context():
user = User.query.filter_by(email='user@test.com').first()
assert user.language == 'en'
assert user.theme == 'dark'
assert user.default_currency == 'EUR'
def test_extended_statistics_payload(logged_user, app):
response = logged_user.get('/analytics/data?year=2026&start_year=2024&end_year=2026')
assert response.status_code == 200
data = response.get_json()
assert 'overview' in data
assert 'comparison' in data
assert 'range_totals' in data

View File

@@ -0,0 +1,44 @@
from datetime import date
from app.extensions import db
from app.models import Category, Expense, User
def test_expense_filters_apply(logged_user, app):
with app.app_context():
user = User.query.filter_by(email='user@test.com').first()
category = Category.query.first()
db.session.add(Expense(user_id=user.id, title='Office Supplies', vendor='Paper Shop', tags='office', amount=20, currency='PLN', purchase_date=date(2026, 3, 12), payment_method='card', category_id=category.id, status='confirmed'))
db.session.add(Expense(user_id=user.id, title='Cinema', vendor='Movie', tags='fun', amount=40, currency='PLN', purchase_date=date(2026, 3, 12), payment_method='cash', status='new'))
db.session.commit()
category_id = category.id
response = logged_user.get(f'/expenses/?year=2026&month=3&q=Paper&category_id={category_id}&payment_method=card')
assert response.status_code == 200
assert b'Office Supplies' in response.data
assert b'Cinema' not in response.data
def test_extended_analytics_payload(logged_user, app):
with app.app_context():
user = User.query.filter_by(email='user@test.com').first()
db.session.add(Expense(user_id=user.id, title='Q1', amount=10, currency='PLN', purchase_date=date(2026, 1, 10), payment_method='card'))
db.session.add(Expense(user_id=user.id, title='Q2', amount=20, currency='PLN', purchase_date=date(2026, 4, 10), payment_method='card'))
db.session.commit()
response = logged_user.get('/analytics/data?year=2026&start_year=2025&end_year=2026')
data = response.get_json()
assert 'quarterly_totals' in data
assert 'weekday_totals' in data
assert any(item['quarter'] == 'Q1' for item in data['quarterly_totals'])
def test_statistics_page_uses_fixed_chart_canvas(logged_user):
response = logged_user.get('/statistics')
assert response.status_code == 200
assert b'chart-canvas' in response.data
def test_create_page_has_mobile_camera_controls(logged_user):
response = logged_user.get('/expenses/create')
assert response.status_code == 200
assert b'cameraCaptureButton' in response.data
assert b'data-mobile-hint' in response.data