Files
TimeTracker/tests/test_payment_routes.py
T
Dries Peeters 90dde470da style: standardize code formatting and normalize line endings
- Normalize line endings from CRLF to LF across all files to match .editorconfig
- Standardize quote style from single quotes to double quotes
- Normalize whitespace and formatting throughout codebase
- Apply consistent code style across 372 files including:
  * Application code (models, routes, services, utils)
  * Test files
  * Configuration files
  * CI/CD workflows

This ensures consistency with the project's .editorconfig settings and
improves code maintainability.
2025-11-28 20:05:37 +01:00

395 lines
14 KiB
Python

"""Tests for Payment routes"""
import pytest
from datetime import datetime, date, timedelta
from decimal import Decimal
from flask import url_for
from app import db, create_app
from app.models import Payment, Invoice, User, Project, Client
from factories import UserFactory, ClientFactory, ProjectFactory, InvoiceFactory, PaymentFactory
from sqlalchemy.pool import StaticPool
@pytest.fixture
def app():
"""Isolated app for payment routes tests using in-memory SQLite to avoid file locks on Windows."""
app = create_app(
{
"TESTING": True,
"WTF_CSRF_ENABLED": False,
"SQLALCHEMY_DATABASE_URI": "sqlite://",
"SQLALCHEMY_ENGINE_OPTIONS": {
"connect_args": {"check_same_thread": False, "timeout": 30},
"poolclass": StaticPool,
},
"SQLALCHEMY_SESSION_OPTIONS": {"expire_on_commit": False},
}
)
with app.app_context():
db.create_all()
try:
db.session.execute("PRAGMA journal_mode=WAL;")
db.session.execute("PRAGMA synchronous=NORMAL;")
db.session.execute("PRAGMA busy_timeout=30000;")
db.session.commit()
except Exception:
db.session.rollback()
try:
yield app
finally:
db.session.remove()
db.drop_all()
try:
db.engine.dispose()
except Exception:
pass
@pytest.fixture
def test_user(app):
"""Create a test user"""
with app.app_context():
user = UserFactory(username="testuser")
yield user
@pytest.fixture
def test_admin(app):
"""Create a test admin user"""
with app.app_context():
admin = UserFactory(username="testadmin", role="admin")
db.session.add(admin)
db.session.commit()
yield admin
@pytest.fixture
def test_client(app):
"""Create a test client"""
with app.app_context():
client = ClientFactory()
yield client
@pytest.fixture
def test_project(app, test_client, test_user):
"""Create a test project"""
with app.app_context():
project = ProjectFactory(client_id=test_client.id, billable=True, hourly_rate=Decimal("100.00"))
yield project
@pytest.fixture
def test_invoice(app, test_project, test_user, test_client):
"""Create a test invoice"""
with app.app_context():
invoice = InvoiceFactory(
project_id=test_project.id,
client_id=test_client.id,
created_by=test_user.id,
client_name="Test Client",
due_date=(date.today() + timedelta(days=30)),
)
invoice.subtotal = Decimal("1000.00")
invoice.tax_rate = Decimal("21.00")
invoice.tax_amount = Decimal("210.00")
invoice.total_amount = Decimal("1210.00")
db.session.add(invoice)
db.session.commit()
yield invoice
@pytest.fixture
def test_payment(app, test_invoice, test_user):
"""Create a test payment"""
with app.app_context():
payment = PaymentFactory(
invoice_id=test_invoice.id,
amount=Decimal("500.00"),
currency="EUR",
payment_date=date.today(),
method="bank_transfer",
reference="REF-12345",
status="completed",
received_by=test_user.id,
)
db.session.add(payment)
db.session.commit()
yield payment
class TestPaymentRoutes:
"""Test payment routes"""
def test_list_payments_requires_login(self, client):
"""Test that listing payments requires login"""
response = client.get("/payments")
assert response.status_code == 302 # Redirect to login
def test_list_payments_as_user(self, client, test_user, test_payment):
"""Test listing payments as a regular user"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# List payments
response = client.get("/payments")
assert response.status_code == 200
def test_list_payments_as_admin(self, client, test_admin, test_payment):
"""Test listing payments as admin"""
with client:
# Login
client.post("/login", data={"username": "testadmin"}, follow_redirects=True)
# List payments
response = client.get("/payments")
assert response.status_code == 200
def test_view_payment_requires_login(self, client, test_payment):
"""Test that viewing a payment requires login"""
response = client.get(f"/payments/{test_payment.id}")
assert response.status_code == 302 # Redirect to login
def test_view_payment(self, client, test_user, test_payment):
"""Test viewing a payment"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# View payment
response = client.get(f"/payments/{test_payment.id}")
assert response.status_code == 200
def test_create_payment_get_requires_login(self, client):
"""Test that creating payment GET requires login"""
response = client.get("/payments/create")
assert response.status_code == 302 # Redirect to login
def test_create_payment_get(self, client, test_user):
"""Test creating payment GET request"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# Get create form
response = client.get("/payments/create")
assert response.status_code == 200
def test_create_payment_post(self, client, test_user, test_invoice, app):
"""Test creating a payment via POST"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# Get CSRF token
response = client.get("/payments/create")
# Create payment
payment_data = {
"invoice_id": test_invoice.id,
"amount": "500.00",
"currency": "EUR",
"payment_date": date.today().strftime("%Y-%m-%d"),
"method": "bank_transfer",
"reference": "TEST-REF-001",
"status": "completed",
"notes": "Test payment",
}
response = client.post("/payments/create", data=payment_data, follow_redirects=True)
assert response.status_code == 200
# Verify payment was created
with app.app_context():
payment = Payment.query.filter_by(reference="TEST-REF-001").first()
assert payment is not None
assert payment.amount == Decimal("500.00")
# Cleanup
db.session.delete(payment)
db.session.commit()
def test_create_payment_with_gateway_fee(self, client, test_user, test_invoice, app):
"""Test creating a payment with gateway fee"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# Create payment with gateway fee
payment_data = {
"invoice_id": test_invoice.id,
"amount": "500.00",
"currency": "EUR",
"payment_date": date.today().strftime("%Y-%m-%d"),
"method": "stripe",
"gateway_fee": "15.00",
"status": "completed",
}
response = client.post("/payments/create", data=payment_data, follow_redirects=True)
assert response.status_code == 200
# Verify payment was created with fee
with app.app_context():
payment = Payment.query.filter_by(invoice_id=test_invoice.id, method="stripe").first()
if payment:
assert payment.gateway_fee == Decimal("15.00")
assert payment.net_amount == Decimal("485.00")
# Cleanup
db.session.delete(payment)
db.session.commit()
def test_edit_payment_get(self, client, test_user, test_payment):
"""Test editing payment GET request"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# Get edit form
response = client.get(f"/payments/{test_payment.id}/edit")
assert response.status_code == 200
def test_edit_payment_post(self, client, test_user, test_payment, app):
"""Test editing a payment via POST"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# Edit payment
payment_data = {
"amount": "600.00",
"currency": "EUR",
"payment_date": date.today().strftime("%Y-%m-%d"),
"method": "cash",
"reference": "UPDATED-REF",
"status": "completed",
"notes": "Updated payment",
}
response = client.post(f"/payments/{test_payment.id}/edit", data=payment_data, follow_redirects=True)
assert response.status_code == 200
# Verify payment was updated
with app.app_context():
payment = Payment.query.get(test_payment.id)
assert payment.amount == Decimal("600.00")
assert payment.method == "cash"
assert payment.reference == "UPDATED-REF"
def test_delete_payment(self, client, test_user, test_payment, app):
"""Test deleting a payment"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# Delete payment
payment_id = test_payment.id
response = client.post(f"/payments/{payment_id}/delete", follow_redirects=True)
assert response.status_code == 200
# Verify payment was deleted
with app.app_context():
payment = Payment.query.get(payment_id)
assert payment is None
def test_payment_stats_api(self, client, test_user, test_payment):
"""Test payment statistics API"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# Get payment stats
response = client.get("/api/payments/stats")
assert response.status_code == 200
data = response.get_json()
assert "total_payments" in data
assert "total_amount" in data
assert "by_method" in data
assert "by_status" in data
def test_create_payment_invalid_amount(self, client, test_user, test_invoice):
"""Test creating payment with invalid amount"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# Try to create payment with invalid amount
payment_data = {
"invoice_id": test_invoice.id,
"amount": "-100.00", # Negative amount
"currency": "EUR",
"payment_date": date.today().strftime("%Y-%m-%d"),
"status": "completed",
}
response = client.post("/payments/create", data=payment_data, follow_redirects=True)
# Should show error message or stay on form
assert response.status_code == 200
def test_create_payment_without_invoice(self, client, test_user):
"""Test creating payment without selecting invoice"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# Try to create payment without invoice
payment_data = {
"amount": "100.00",
"currency": "EUR",
"payment_date": date.today().strftime("%Y-%m-%d"),
"status": "completed",
}
response = client.post("/payments/create", data=payment_data, follow_redirects=True)
# Should show error or stay on form
assert response.status_code == 200
class TestPaymentFilteringAndSearch:
"""Test payment filtering and search functionality"""
def test_filter_payments_by_status(self, client, test_user, test_payment):
"""Test filtering payments by status"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# Filter by status
response = client.get("/payments?status=completed")
assert response.status_code == 200
def test_filter_payments_by_method(self, client, test_user, test_payment):
"""Test filtering payments by method"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# Filter by method
response = client.get("/payments?method=bank_transfer")
assert response.status_code == 200
def test_filter_payments_by_date_range(self, client, test_user, test_payment):
"""Test filtering payments by date range"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# Filter by date range
date_from = (date.today() - timedelta(days=7)).strftime("%Y-%m-%d")
date_to = date.today().strftime("%Y-%m-%d")
response = client.get(f"/payments?date_from={date_from}&date_to={date_to}")
assert response.status_code == 200
def test_filter_payments_by_invoice(self, client, test_user, test_invoice, test_payment):
"""Test filtering payments by invoice"""
with client:
# Login
client.post("/login", data={"username": "testuser"}, follow_redirects=True)
# Filter by invoice
response = client.get(f"/payments?invoice_id={test_invoice.id}")
assert response.status_code == 200