mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-27 06:58:51 -06:00
- Fix keyboard shortcuts (like 'g r' for Go to Reports) incorrectly triggering while typing in input fields, textareas, and rich text editors - Enhance detection for popular rich text editors: * Toast UI Editor (used in project descriptions) * TinyMCE, Quill, CodeMirror, Summernote * All contenteditable elements - Allow specific global shortcuts even in input fields: * Ctrl+K / Cmd+K: Open command palette * Shift+?: Show keyboard shortcuts help * Ctrl+/: Focus search - Clear key sequences when user starts typing to prevent partial matches - Add debug logging for troubleshooting keyboard shortcut issues - Update JavaScript cache busting version numbers (v=2.0, v=2.2) Test improvements: - Add comprehensive test suite for keyboard shortcuts input fix * Test typing 'gr' in 'program' doesn't trigger navigation * Test rich text editor detection logic * Test allowed shortcuts in inputs - Refactor smoke tests to use admin_authenticated_client fixture instead of manual login (DRY principle) - Fix Windows PermissionError in test cleanup for temporary files - Add SESSION_COOKIE_HTTPONLY to test config for security - Update test secret key length to meet requirements - Remove duplicate admin user fixtures Resolves issue where typing words like 'program' or 'graphics' in forms would trigger unintended navigation shortcuts.
382 lines
12 KiB
Python
382 lines
12 KiB
Python
"""Tests for PDF layout customization functionality."""
|
|
|
|
import pytest
|
|
from datetime import date, timedelta
|
|
from decimal import Decimal
|
|
from app import db
|
|
from app.models import User, Project, Invoice, InvoiceItem, Settings, Client
|
|
from flask import url_for
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_user(app):
|
|
"""Create an admin user for testing."""
|
|
user = User(username='admin', role='admin', email='admin@test.com')
|
|
user.is_active = True
|
|
user.set_password('password123')
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def regular_user(app):
|
|
"""Create a regular user for testing."""
|
|
user = User(username='regular', role='user', email='regular@test.com')
|
|
user.is_active = True
|
|
user.set_password('password123')
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_invoice(app, admin_user):
|
|
"""Create a sample invoice for testing."""
|
|
# Create a client
|
|
client = Client(name='Test Client', email='client@test.com')
|
|
db.session.add(client)
|
|
db.session.commit()
|
|
|
|
# Create a project
|
|
project = Project(
|
|
name='Test Project',
|
|
client='Test Client',
|
|
description='Test project for PDF',
|
|
billable=True,
|
|
hourly_rate=Decimal('100.00')
|
|
)
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
|
|
# Create invoice
|
|
invoice = Invoice(
|
|
invoice_number='INV-2024-001',
|
|
project_id=project.id,
|
|
client_name='Test Client',
|
|
client_email='client@test.com',
|
|
client_address='123 Test St',
|
|
due_date=date.today() + timedelta(days=30),
|
|
created_by=admin_user.id,
|
|
client_id=client.id,
|
|
tax_rate=Decimal('10.00'),
|
|
notes='Test notes',
|
|
terms='Test terms'
|
|
)
|
|
db.session.add(invoice)
|
|
db.session.commit()
|
|
|
|
# Add invoice item
|
|
item = InvoiceItem(
|
|
invoice_id=invoice.id,
|
|
description='Test Service',
|
|
quantity=Decimal('5.00'),
|
|
unit_price=Decimal('100.00')
|
|
)
|
|
db.session.add(item)
|
|
db.session.commit()
|
|
|
|
return invoice
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.admin
|
|
def test_pdf_layout_page_requires_admin(client, regular_user):
|
|
"""Test that PDF layout page requires admin access."""
|
|
with client:
|
|
# Login as regular user
|
|
client.post('/auth/login', data={
|
|
'username': 'regular',
|
|
'password': 'password123'
|
|
})
|
|
|
|
# Try to access PDF layout page
|
|
response = client.get('/admin/pdf-layout')
|
|
|
|
# Should redirect or show forbidden
|
|
assert response.status_code in [302, 403]
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.admin
|
|
def test_pdf_layout_page_accessible_to_admin(admin_authenticated_client):
|
|
"""Test that PDF layout page is accessible to admin."""
|
|
# Access PDF layout page
|
|
response = admin_authenticated_client.get('/admin/pdf-layout')
|
|
|
|
assert response.status_code == 200
|
|
assert b'PDF Layout Editor' in response.data or b'pdf' in response.data.lower()
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.admin
|
|
def test_pdf_layout_save_custom_template(admin_authenticated_client, app):
|
|
"""Test saving custom PDF layout templates."""
|
|
custom_html = '<div class="custom-invoice"><h1>{{ invoice.invoice_number }}</h1></div>'
|
|
custom_css = '.custom-invoice { color: red; }'
|
|
|
|
# Save custom template
|
|
response = admin_authenticated_client.post('/admin/pdf-layout', data={
|
|
'invoice_pdf_template_html': custom_html,
|
|
'invoice_pdf_template_css': custom_css
|
|
}, follow_redirects=True)
|
|
|
|
assert response.status_code == 200
|
|
|
|
# Verify settings were saved
|
|
settings = Settings.get_settings()
|
|
assert settings.invoice_pdf_template_html == custom_html
|
|
assert settings.invoice_pdf_template_css == custom_css
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.admin
|
|
def test_pdf_layout_reset_to_defaults(admin_authenticated_client, app):
|
|
"""Test resetting PDF layout to defaults."""
|
|
# First, set custom templates
|
|
settings = Settings.get_settings()
|
|
settings.invoice_pdf_template_html = '<div>Custom HTML</div>'
|
|
settings.invoice_pdf_template_css = 'body { color: blue; }'
|
|
db.session.commit()
|
|
|
|
# Reset to defaults
|
|
response = admin_authenticated_client.post('/admin/pdf-layout/reset', follow_redirects=True)
|
|
|
|
assert response.status_code == 200
|
|
|
|
# Verify templates were cleared
|
|
settings = Settings.get_settings()
|
|
assert settings.invoice_pdf_template_html == ''
|
|
assert settings.invoice_pdf_template_css == ''
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.admin
|
|
def test_pdf_layout_get_defaults(admin_authenticated_client):
|
|
"""Test getting default PDF layout templates."""
|
|
# Get default templates
|
|
response = admin_authenticated_client.get('/admin/pdf-layout/default')
|
|
|
|
assert response.status_code == 200
|
|
assert response.is_json
|
|
|
|
data = response.get_json()
|
|
assert 'html' in data
|
|
assert 'css' in data
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.admin
|
|
def test_pdf_layout_preview(admin_authenticated_client, sample_invoice):
|
|
"""Test PDF layout preview functionality."""
|
|
# Test preview with custom HTML/CSS
|
|
response = admin_authenticated_client.post('/admin/pdf-layout/preview', data={
|
|
'html': '<h1>Test Invoice {{ invoice.invoice_number }}</h1>',
|
|
'css': 'h1 { color: red; }',
|
|
'invoice_id': sample_invoice.id
|
|
})
|
|
|
|
assert response.status_code == 200
|
|
# Should return HTML content
|
|
assert b'Test Invoice' in response.data or b'INV-2024-001' in response.data
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.admin
|
|
def test_pdf_layout_preview_with_mock_invoice(admin_authenticated_client, app):
|
|
"""Test PDF layout preview with mock invoice when no real invoice exists."""
|
|
# Delete all invoices
|
|
Invoice.query.delete()
|
|
db.session.commit()
|
|
|
|
# Test preview should still work with mock invoice
|
|
response = admin_authenticated_client.post('/admin/pdf-layout/preview', data={
|
|
'html': '<h1>{{ invoice.invoice_number }}</h1>',
|
|
'css': 'h1 { color: blue; }'
|
|
})
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
@pytest.mark.models
|
|
def test_settings_pdf_template_fields_exist(app):
|
|
"""Test that Settings model has PDF template fields."""
|
|
settings = Settings.get_settings()
|
|
|
|
assert hasattr(settings, 'invoice_pdf_template_html')
|
|
assert hasattr(settings, 'invoice_pdf_template_css')
|
|
|
|
|
|
@pytest.mark.models
|
|
def test_settings_pdf_template_defaults(app):
|
|
"""Test that PDF template fields have proper defaults."""
|
|
settings = Settings.get_settings()
|
|
|
|
# Should default to empty strings
|
|
if not settings.invoice_pdf_template_html:
|
|
assert settings.invoice_pdf_template_html == '' or settings.invoice_pdf_template_html is None
|
|
if not settings.invoice_pdf_template_css:
|
|
assert settings.invoice_pdf_template_css == '' or settings.invoice_pdf_template_css is None
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_pdf_generation_with_custom_template(app, sample_invoice):
|
|
"""Test PDF generation uses custom templates when available."""
|
|
from app.utils.pdf_generator import InvoicePDFGenerator
|
|
|
|
# Set custom template
|
|
settings = Settings.get_settings()
|
|
settings.invoice_pdf_template_html = '''
|
|
<div class="custom-wrapper">
|
|
<h1>Custom Invoice: {{ invoice.invoice_number }}</h1>
|
|
<p>Client: {{ invoice.client_name }}</p>
|
|
</div>
|
|
'''
|
|
settings.invoice_pdf_template_css = '''
|
|
.custom-wrapper { padding: 20px; }
|
|
h1 { color: #333; }
|
|
'''
|
|
db.session.commit()
|
|
|
|
# Generate PDF
|
|
generator = InvoicePDFGenerator(sample_invoice, settings)
|
|
pdf_bytes = generator.generate_pdf()
|
|
|
|
# Should generate valid PDF
|
|
assert pdf_bytes is not None
|
|
assert len(pdf_bytes) > 0
|
|
# PDF files start with %PDF
|
|
assert pdf_bytes[:4] == b'%PDF'
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_pdf_generation_with_default_template(app, sample_invoice):
|
|
"""Test PDF generation uses default template when no custom template set."""
|
|
from app.utils.pdf_generator import InvoicePDFGenerator
|
|
|
|
# Clear any custom templates
|
|
settings = Settings.get_settings()
|
|
settings.invoice_pdf_template_html = ''
|
|
settings.invoice_pdf_template_css = ''
|
|
db.session.commit()
|
|
|
|
# Generate PDF
|
|
generator = InvoicePDFGenerator(sample_invoice, settings)
|
|
pdf_bytes = generator.generate_pdf()
|
|
|
|
# Should generate valid PDF
|
|
assert pdf_bytes is not None
|
|
assert len(pdf_bytes) > 0
|
|
# PDF files start with %PDF
|
|
assert pdf_bytes[:4] == b'%PDF'
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.admin
|
|
def test_pdf_layout_navigation_link_exists(admin_authenticated_client):
|
|
"""Test that PDF layout link exists in admin navigation."""
|
|
# Access admin dashboard or any admin page
|
|
response = admin_authenticated_client.get('/admin/settings')
|
|
|
|
assert response.status_code == 200
|
|
# Should contain link to PDF layout page
|
|
# The link might be in the navigation or as a menu item
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.admin
|
|
def test_pdf_layout_form_csrf_protection(admin_authenticated_client):
|
|
"""Test that PDF layout form has CSRF protection."""
|
|
# Get the PDF layout page
|
|
response = admin_authenticated_client.get('/admin/pdf-layout')
|
|
|
|
assert response.status_code == 200
|
|
# Should contain CSRF token
|
|
assert b'csrf_token' in response.data or b'name="csrf_token"' in response.data
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_pdf_layout_jinja_variable_rendering(app, sample_invoice):
|
|
"""Test that Jinja variables are properly rendered in custom templates."""
|
|
from app.utils.pdf_generator import InvoicePDFGenerator
|
|
|
|
# Set custom template with various Jinja variables
|
|
settings = Settings.get_settings()
|
|
settings.invoice_pdf_template_html = '''
|
|
<div>
|
|
<h1>Invoice: {{ invoice.invoice_number }}</h1>
|
|
<p>Client: {{ invoice.client_name }}</p>
|
|
<p>Company: {{ settings.company_name }}</p>
|
|
<p>Total: {{ format_money(invoice.total_amount) }}</p>
|
|
</div>
|
|
'''
|
|
db.session.commit()
|
|
|
|
# Generate PDF
|
|
generator = InvoicePDFGenerator(sample_invoice, settings)
|
|
pdf_bytes = generator.generate_pdf()
|
|
|
|
# Should generate valid PDF without errors
|
|
assert pdf_bytes is not None
|
|
assert len(pdf_bytes) > 0
|
|
|
|
|
|
@pytest.mark.smoke
|
|
@pytest.mark.admin
|
|
def test_pdf_layout_rate_limiting(admin_authenticated_client):
|
|
"""Test that PDF layout endpoints have rate limiting."""
|
|
# Make multiple rapid requests to preview endpoint
|
|
for i in range(65): # Exceeds the 60 per minute limit
|
|
response = admin_authenticated_client.post('/admin/pdf-layout/preview', data={
|
|
'html': '<h1>Test</h1>',
|
|
'css': 'h1 { color: red; }'
|
|
})
|
|
|
|
# After 60 requests, should be rate limited
|
|
if i >= 60:
|
|
assert response.status_code == 429 # Too Many Requests
|
|
break
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_pdf_layout_with_invoice_items_loop(app, sample_invoice):
|
|
"""Test custom template with loop over invoice items."""
|
|
from app.utils.pdf_generator import InvoicePDFGenerator
|
|
|
|
# Set custom template with items loop
|
|
settings = Settings.get_settings()
|
|
settings.invoice_pdf_template_html = '''
|
|
<div>
|
|
<h1>Invoice: {{ invoice.invoice_number }}</h1>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Description</th>
|
|
<th>Quantity</th>
|
|
<th>Price</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for item in invoice.items %}
|
|
<tr>
|
|
<td>{{ item.description }}</td>
|
|
<td>{{ item.quantity }}</td>
|
|
<td>{{ format_money(item.total_amount) }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
'''
|
|
db.session.commit()
|
|
|
|
# Generate PDF
|
|
generator = InvoicePDFGenerator(sample_invoice, settings)
|
|
pdf_bytes = generator.generate_pdf()
|
|
|
|
# Should generate valid PDF
|
|
assert pdf_bytes is not None
|
|
assert len(pdf_bytes) > 0
|
|
assert pdf_bytes[:4] == b'%PDF'
|
|
|