first commit
This commit is contained in:
49
tests/conftest.py
Normal file
49
tests/conftest.py
Normal 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
56
tests/test_admin.py
Normal 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
|
||||
5
tests/test_admin_audit.py
Normal file
5
tests/test_admin_audit.py
Normal 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
24
tests/test_auth.py
Normal 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
32
tests/test_errors.py
Normal 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
110
tests/test_expenses.py
Normal 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
|
||||
89
tests/test_integrations.py
Normal file
89
tests/test_integrations.py
Normal 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
40
tests/test_main.py
Normal 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
|
||||
44
tests/test_premium_features.py
Normal file
44
tests/test_premium_features.py
Normal 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
|
||||
Reference in New Issue
Block a user