mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-22 14:20:47 -05:00
90dde470da
- 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.
395 lines
14 KiB
Python
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
|