mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-28 07:28:59 -06:00
- Fix script block name from extra_js to scripts_extra to match base.html - Replace inline onclick handlers with event listeners to fix scope issues - Fix ReferenceError for toggleViewMode and insertVariable functions - Improve editor initialization flow with proper script loading detection - Add error handling and fallback to textarea if Toast UI Editor fails to load - Add debug logging for troubleshooting initialization issues - Ensure default templates are editable (no restrictions in backend) - Add email templates link to admin menu in base.html - Remove ENV file configuration details from email support page The editor now properly initializes and all interactive features work correctly.
467 lines
18 KiB
Python
467 lines
18 KiB
Python
"""
|
|
Tests for invoice email sending functionality
|
|
"""
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
from datetime import datetime, date, timedelta
|
|
from decimal import Decimal
|
|
from flask import current_app
|
|
from app import db
|
|
from app.models import Invoice, InvoiceEmail, User, Settings, Client, Project
|
|
from app.utils.email import send_invoice_email
|
|
from factories import UserFactory, ClientFactory, ProjectFactory, InvoiceFactory, InvoiceItemFactory
|
|
|
|
|
|
@pytest.fixture
|
|
def test_user(app):
|
|
"""Create a test user"""
|
|
user = UserFactory(username='testuser', role='user')
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def test_client(app):
|
|
"""Create a test client"""
|
|
client = ClientFactory(name='Test Client', email='client@test.com')
|
|
db.session.commit()
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def test_project(app, test_client):
|
|
"""Create a test project"""
|
|
project = ProjectFactory(
|
|
name='Test Project',
|
|
client_id=test_client.id,
|
|
billable=True,
|
|
hourly_rate=Decimal('100.00')
|
|
)
|
|
db.session.commit()
|
|
return project
|
|
|
|
|
|
@pytest.fixture
|
|
def test_invoice(app, test_user, test_project, test_client):
|
|
"""Create a test invoice with items"""
|
|
invoice = InvoiceFactory(
|
|
invoice_number='INV-2024-001',
|
|
project_id=test_project.id,
|
|
client_id=test_client.id,
|
|
client_name=test_client.name,
|
|
client_email=test_client.email,
|
|
due_date=date.today() + timedelta(days=30),
|
|
created_by=test_user.id,
|
|
status='draft',
|
|
subtotal=Decimal('1000.00'),
|
|
tax_rate=Decimal('20.00'),
|
|
tax_amount=Decimal('200.00'),
|
|
total_amount=Decimal('1200.00'),
|
|
currency_code='EUR'
|
|
)
|
|
db.session.commit()
|
|
|
|
# Add invoice item
|
|
item = InvoiceItemFactory(
|
|
invoice_id=invoice.id,
|
|
description='Test Service',
|
|
quantity=Decimal('10.00'),
|
|
unit_price=Decimal('100.00'),
|
|
total_amount=Decimal('1000.00')
|
|
)
|
|
db.session.commit()
|
|
|
|
return invoice
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_pdf_generator():
|
|
"""Mock PDF generator"""
|
|
with patch('app.utils.email.InvoicePDFGenerator') as mock_gen:
|
|
mock_instance = MagicMock()
|
|
mock_instance.generate_pdf.return_value = b'fake_pdf_bytes'
|
|
mock_gen.return_value = mock_instance
|
|
yield mock_instance
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_mail_send():
|
|
"""Mock mail.send"""
|
|
with patch('app.utils.email.mail.send') as mock_send:
|
|
yield mock_send
|
|
|
|
|
|
class TestSendInvoiceEmail:
|
|
"""Tests for send_invoice_email function"""
|
|
|
|
def test_send_invoice_email_success(self, app, test_invoice, test_user, mock_pdf_generator, mock_mail_send):
|
|
"""Test successfully sending an invoice email"""
|
|
with app.app_context():
|
|
# Configure mail server
|
|
current_app.config['MAIL_SERVER'] = 'smtp.test.com'
|
|
current_app.config['MAIL_DEFAULT_SENDER'] = 'noreply@test.com'
|
|
|
|
success, invoice_email, message = send_invoice_email(
|
|
invoice=test_invoice,
|
|
recipient_email='client@test.com',
|
|
sender_user=test_user
|
|
)
|
|
|
|
assert success is True
|
|
assert invoice_email is not None
|
|
assert invoice_email.recipient_email == 'client@test.com'
|
|
assert invoice_email.invoice_id == test_invoice.id
|
|
assert invoice_email.sent_by == test_user.id
|
|
assert invoice_email.status == 'sent'
|
|
assert 'successfully' in message.lower()
|
|
assert mock_mail_send.called
|
|
|
|
# Verify invoice status was updated
|
|
db.session.refresh(test_invoice)
|
|
assert test_invoice.status == 'sent'
|
|
|
|
def test_send_invoice_email_with_custom_message(self, app, test_invoice, test_user, mock_pdf_generator, mock_mail_send):
|
|
"""Test sending invoice email with custom message"""
|
|
with app.app_context():
|
|
current_app.config['MAIL_SERVER'] = 'smtp.test.com'
|
|
current_app.config['MAIL_DEFAULT_SENDER'] = 'noreply@test.com'
|
|
|
|
custom_message = "Thank you for your business!"
|
|
success, invoice_email, message = send_invoice_email(
|
|
invoice=test_invoice,
|
|
recipient_email='client@test.com',
|
|
sender_user=test_user,
|
|
custom_message=custom_message
|
|
)
|
|
|
|
assert success is True
|
|
assert invoice_email is not None
|
|
# Verify the message was sent (check mail.send was called with message containing custom text)
|
|
assert mock_mail_send.called
|
|
|
|
def test_send_invoice_email_pdf_generation_failure(self, app, test_invoice, test_user, mock_mail_send):
|
|
"""Test handling PDF generation failure"""
|
|
with app.app_context():
|
|
current_app.config['MAIL_SERVER'] = 'smtp.test.com'
|
|
current_app.config['MAIL_DEFAULT_SENDER'] = 'noreply@test.com'
|
|
|
|
# Mock PDF generator to fail
|
|
with patch('app.utils.email.InvoicePDFGenerator') as mock_gen:
|
|
mock_instance = MagicMock()
|
|
mock_instance.generate_pdf.side_effect = Exception("PDF generation failed")
|
|
mock_gen.return_value = mock_instance
|
|
|
|
# Mock fallback generator to also fail
|
|
with patch('app.utils.email.InvoicePDFGeneratorFallback') as mock_fallback:
|
|
mock_fallback_instance = MagicMock()
|
|
mock_fallback_instance.generate_pdf.side_effect = Exception("Fallback failed")
|
|
mock_fallback.return_value = mock_fallback_instance
|
|
|
|
success, invoice_email, message = send_invoice_email(
|
|
invoice=test_invoice,
|
|
recipient_email='client@test.com',
|
|
sender_user=test_user
|
|
)
|
|
|
|
assert success is False
|
|
assert invoice_email is None
|
|
assert 'pdf generation failed' in message.lower() or 'failed' in message.lower()
|
|
|
|
def test_send_invoice_email_no_mail_server(self, app, test_invoice, test_user, mock_pdf_generator):
|
|
"""Test sending email when mail server is not configured"""
|
|
with app.app_context():
|
|
current_app.config['MAIL_SERVER'] = None
|
|
|
|
success, invoice_email, message = send_invoice_email(
|
|
invoice=test_invoice,
|
|
recipient_email='client@test.com',
|
|
sender_user=test_user
|
|
)
|
|
|
|
# Should still attempt to send but may fail gracefully
|
|
# The function should handle this case
|
|
assert invoice_email is not None or success is False
|
|
|
|
def test_send_invoice_email_creates_tracking_record(self, app, test_invoice, test_user, mock_pdf_generator, mock_mail_send):
|
|
"""Test that email tracking record is created"""
|
|
with app.app_context():
|
|
current_app.config['MAIL_SERVER'] = 'smtp.test.com'
|
|
current_app.config['MAIL_DEFAULT_SENDER'] = 'noreply@test.com'
|
|
|
|
# Count existing records
|
|
initial_count = InvoiceEmail.query.filter_by(invoice_id=test_invoice.id).count()
|
|
|
|
success, invoice_email, message = send_invoice_email(
|
|
invoice=test_invoice,
|
|
recipient_email='client@test.com',
|
|
sender_user=test_user
|
|
)
|
|
|
|
assert success is True
|
|
|
|
# Verify record was created
|
|
final_count = InvoiceEmail.query.filter_by(invoice_id=test_invoice.id).count()
|
|
assert final_count == initial_count + 1
|
|
|
|
# Verify record details
|
|
assert invoice_email.recipient_email == 'client@test.com'
|
|
assert invoice_email.invoice_id == test_invoice.id
|
|
assert invoice_email.sent_by == test_user.id
|
|
|
|
def test_send_invoice_email_updates_draft_status(self, app, test_invoice, test_user, mock_pdf_generator, mock_mail_send):
|
|
"""Test that draft invoice status is updated to 'sent'"""
|
|
with app.app_context():
|
|
current_app.config['MAIL_SERVER'] = 'smtp.test.com'
|
|
current_app.config['MAIL_DEFAULT_SENDER'] = 'noreply@test.com'
|
|
|
|
# Ensure invoice is in draft status
|
|
test_invoice.status = 'draft'
|
|
db.session.commit()
|
|
|
|
success, invoice_email, message = send_invoice_email(
|
|
invoice=test_invoice,
|
|
recipient_email='client@test.com',
|
|
sender_user=test_user
|
|
)
|
|
|
|
assert success is True
|
|
|
|
# Verify status was updated
|
|
db.session.refresh(test_invoice)
|
|
assert test_invoice.status == 'sent'
|
|
|
|
def test_send_invoice_email_does_not_update_non_draft_status(self, app, test_invoice, test_user, mock_pdf_generator, mock_mail_send):
|
|
"""Test that non-draft invoice status is not changed"""
|
|
with app.app_context():
|
|
current_app.config['MAIL_SERVER'] = 'smtp.test.com'
|
|
current_app.config['MAIL_DEFAULT_SENDER'] = 'noreply@test.com'
|
|
|
|
# Set invoice to 'sent' status
|
|
test_invoice.status = 'sent'
|
|
db.session.commit()
|
|
|
|
success, invoice_email, message = send_invoice_email(
|
|
invoice=test_invoice,
|
|
recipient_email='client@test.com',
|
|
sender_user=test_user
|
|
)
|
|
|
|
assert success is True
|
|
|
|
# Verify status remained 'sent'
|
|
db.session.refresh(test_invoice)
|
|
assert test_invoice.status == 'sent'
|
|
|
|
def test_send_invoice_email_with_email_template(self, app, test_invoice, test_user, mock_pdf_generator, mock_mail_send):
|
|
"""Test sending invoice email with custom email template"""
|
|
with app.app_context():
|
|
from app.models import InvoiceTemplate
|
|
|
|
current_app.config['MAIL_SERVER'] = 'smtp.test.com'
|
|
current_app.config['MAIL_DEFAULT_SENDER'] = 'noreply@test.com'
|
|
|
|
# Create an email template
|
|
template = InvoiceTemplate(
|
|
name='Test Template',
|
|
html='<html><body><h1>Invoice {{ invoice.invoice_number }}</h1></body></html>',
|
|
css='body { color: black; }'
|
|
)
|
|
db.session.add(template)
|
|
db.session.commit()
|
|
|
|
success, invoice_email, message = send_invoice_email(
|
|
invoice=test_invoice,
|
|
recipient_email='client@test.com',
|
|
sender_user=test_user,
|
|
email_template_id=template.id
|
|
)
|
|
|
|
assert success is True
|
|
assert invoice_email is not None
|
|
assert mock_mail_send.called
|
|
|
|
def test_send_invoice_email_failure_creates_failed_record(self, app, test_invoice, test_user, mock_pdf_generator):
|
|
"""Test that failed email sends create a failed tracking record"""
|
|
with app.app_context():
|
|
current_app.config['MAIL_SERVER'] = 'smtp.test.com'
|
|
current_app.config['MAIL_DEFAULT_SENDER'] = 'noreply@test.com'
|
|
|
|
# Mock mail.send to raise an exception
|
|
with patch('app.utils.email.mail.send') as mock_send:
|
|
mock_send.side_effect = Exception("SMTP connection failed")
|
|
|
|
success, invoice_email, message = send_invoice_email(
|
|
invoice=test_invoice,
|
|
recipient_email='client@test.com',
|
|
sender_user=test_user
|
|
)
|
|
|
|
assert success is False
|
|
# Should create a failed record
|
|
failed_record = InvoiceEmail.query.filter_by(
|
|
invoice_id=test_invoice.id,
|
|
status='failed'
|
|
).first()
|
|
assert failed_record is not None
|
|
assert failed_record.error_message is not None
|
|
|
|
|
|
class TestInvoiceEmailRoutes:
|
|
"""Tests for invoice email routes"""
|
|
|
|
def test_send_invoice_email_route_success(self, client, test_user, test_invoice, mock_pdf_generator, mock_mail_send):
|
|
"""Test the send invoice email route"""
|
|
with client.session_transaction() as sess:
|
|
sess['_user_id'] = str(test_user.id)
|
|
sess['_fresh'] = True
|
|
|
|
response = client.post(
|
|
f'/invoices/{test_invoice.id}/send-email',
|
|
data={
|
|
'recipient_email': 'client@test.com',
|
|
'csrf_token': 'test_token'
|
|
}
|
|
)
|
|
|
|
# Should return success (may need to handle CSRF token properly in test)
|
|
assert response.status_code in [200, 400, 403] # 400/403 if CSRF fails
|
|
|
|
def test_get_invoice_email_history(self, client, test_user, test_invoice, mock_pdf_generator, mock_mail_send):
|
|
"""Test getting invoice email history"""
|
|
with client.session_transaction() as sess:
|
|
sess['_user_id'] = str(test_user.id)
|
|
sess['_fresh'] = True
|
|
|
|
# First send an email
|
|
with client.application.app_context():
|
|
from app.utils.email import send_invoice_email
|
|
current_app.config['MAIL_SERVER'] = 'smtp.test.com'
|
|
current_app.config['MAIL_DEFAULT_SENDER'] = 'noreply@test.com'
|
|
|
|
send_invoice_email(
|
|
invoice=test_invoice,
|
|
recipient_email='client@test.com',
|
|
sender_user=test_user
|
|
)
|
|
|
|
# Then get history
|
|
response = client.get(f'/invoices/{test_invoice.id}/email-history')
|
|
|
|
# Should return success (may need to handle authentication properly)
|
|
assert response.status_code in [200, 401, 403]
|
|
|
|
def test_resend_invoice_email_route(self, client, test_user, test_invoice, mock_pdf_generator, mock_mail_send):
|
|
"""Test the resend invoice email route"""
|
|
with client.session_transaction() as sess:
|
|
sess['_user_id'] = str(test_user.id)
|
|
sess['_fresh'] = True
|
|
|
|
# First send an email to create a record
|
|
with client.application.app_context():
|
|
from app.utils.email import send_invoice_email
|
|
current_app.config['MAIL_SERVER'] = 'smtp.test.com'
|
|
current_app.config['MAIL_DEFAULT_SENDER'] = 'noreply@test.com'
|
|
|
|
success, invoice_email, _ = send_invoice_email(
|
|
invoice=test_invoice,
|
|
recipient_email='client@test.com',
|
|
sender_user=test_user
|
|
)
|
|
|
|
if success and invoice_email:
|
|
# Then resend it
|
|
response = client.post(
|
|
f'/invoices/{test_invoice.id}/resend-email/{invoice_email.id}',
|
|
data={
|
|
'recipient_email': 'client@test.com',
|
|
'csrf_token': 'test_token'
|
|
}
|
|
)
|
|
|
|
# Should return success (may need to handle CSRF token properly)
|
|
assert response.status_code in [200, 400, 403]
|
|
|
|
|
|
class TestInvoiceEmailModel:
|
|
"""Tests for InvoiceEmail model"""
|
|
|
|
def test_invoice_email_creation(self, app, test_invoice, test_user):
|
|
"""Test creating an InvoiceEmail record"""
|
|
with app.app_context():
|
|
invoice_email = InvoiceEmail(
|
|
invoice_id=test_invoice.id,
|
|
recipient_email='client@test.com',
|
|
subject='Test Invoice',
|
|
sent_by=test_user.id
|
|
)
|
|
db.session.add(invoice_email)
|
|
db.session.commit()
|
|
|
|
assert invoice_email.id is not None
|
|
assert invoice_email.invoice_id == test_invoice.id
|
|
assert invoice_email.recipient_email == 'client@test.com'
|
|
assert invoice_email.status == 'sent'
|
|
assert invoice_email.sent_at is not None
|
|
|
|
def test_invoice_email_mark_opened(self, app, test_invoice, test_user):
|
|
"""Test marking email as opened"""
|
|
with app.app_context():
|
|
invoice_email = InvoiceEmail(
|
|
invoice_id=test_invoice.id,
|
|
recipient_email='client@test.com',
|
|
subject='Test Invoice',
|
|
sent_by=test_user.id
|
|
)
|
|
db.session.add(invoice_email)
|
|
db.session.commit()
|
|
|
|
invoice_email.mark_opened()
|
|
db.session.commit()
|
|
|
|
assert invoice_email.status == 'opened'
|
|
assert invoice_email.opened_at is not None
|
|
assert invoice_email.opened_count == 1
|
|
|
|
def test_invoice_email_mark_failed(self, app, test_invoice, test_user):
|
|
"""Test marking email as failed"""
|
|
with app.app_context():
|
|
invoice_email = InvoiceEmail(
|
|
invoice_id=test_invoice.id,
|
|
recipient_email='client@test.com',
|
|
subject='Test Invoice',
|
|
sent_by=test_user.id
|
|
)
|
|
db.session.add(invoice_email)
|
|
db.session.commit()
|
|
|
|
error_message = "SMTP connection failed"
|
|
invoice_email.mark_failed(error_message)
|
|
db.session.commit()
|
|
|
|
assert invoice_email.status == 'failed'
|
|
assert invoice_email.error_message == error_message
|
|
|
|
def test_invoice_email_to_dict(self, app, test_invoice, test_user):
|
|
"""Test converting InvoiceEmail to dictionary"""
|
|
with app.app_context():
|
|
invoice_email = InvoiceEmail(
|
|
invoice_id=test_invoice.id,
|
|
recipient_email='client@test.com',
|
|
subject='Test Invoice',
|
|
sent_by=test_user.id
|
|
)
|
|
db.session.add(invoice_email)
|
|
db.session.commit()
|
|
|
|
email_dict = invoice_email.to_dict()
|
|
|
|
assert isinstance(email_dict, dict)
|
|
assert email_dict['invoice_id'] == test_invoice.id
|
|
assert email_dict['recipient_email'] == 'client@test.com'
|
|
assert email_dict['subject'] == 'Test Invoice'
|
|
assert email_dict['status'] == 'sent'
|
|
assert 'sent_at' in email_dict
|
|
assert 'created_at' in email_dict
|
|
|