mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-02-08 13:18: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.
575 lines
16 KiB
Python
575 lines
16 KiB
Python
"""
|
|
Pytest configuration and shared fixtures for TimeTracker tests.
|
|
This file contains common fixtures and test configuration used across all test modules.
|
|
"""
|
|
|
|
import pytest
|
|
import os
|
|
import tempfile
|
|
from datetime import datetime, timedelta
|
|
from decimal import Decimal
|
|
|
|
from app import create_app, db
|
|
from app.models import (
|
|
User, Project, TimeEntry, Client, Settings,
|
|
Invoice, InvoiceItem, Task
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Application Fixtures
|
|
# ============================================================================
|
|
|
|
@pytest.fixture(scope='session')
|
|
def app_config():
|
|
"""Base test configuration."""
|
|
return {
|
|
'TESTING': True,
|
|
'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:',
|
|
'SQLALCHEMY_TRACK_MODIFICATIONS': False,
|
|
'WTF_CSRF_ENABLED': False,
|
|
'SECRET_KEY': 'test-secret-key-do-not-use-in-production',
|
|
'SERVER_NAME': 'localhost:5000',
|
|
'APPLICATION_ROOT': '/',
|
|
'PREFERRED_URL_SCHEME': 'http',
|
|
'SESSION_COOKIE_HTTPONLY': True,
|
|
}
|
|
|
|
|
|
@pytest.fixture(scope='function')
|
|
def app(app_config):
|
|
"""Create application for testing with function scope."""
|
|
app = create_app(app_config)
|
|
|
|
with app.app_context():
|
|
db.create_all()
|
|
|
|
# Create default settings
|
|
settings = Settings()
|
|
db.session.add(settings)
|
|
db.session.commit()
|
|
|
|
yield app
|
|
|
|
db.session.remove()
|
|
db.drop_all()
|
|
|
|
|
|
@pytest.fixture(scope='function')
|
|
def client(app):
|
|
"""Create test client."""
|
|
return app.test_client()
|
|
|
|
|
|
@pytest.fixture(scope='function')
|
|
def runner(app):
|
|
"""Create test CLI runner."""
|
|
return app.test_cli_runner()
|
|
|
|
|
|
# ============================================================================
|
|
# Database Fixtures
|
|
# ============================================================================
|
|
|
|
@pytest.fixture(scope='function')
|
|
def db_session(app):
|
|
"""Create a database session for tests."""
|
|
with app.app_context():
|
|
yield db.session
|
|
|
|
|
|
# ============================================================================
|
|
# User Fixtures
|
|
# ============================================================================
|
|
|
|
@pytest.fixture
|
|
def user(app):
|
|
"""Create a regular test user."""
|
|
# Idempotent: return existing test user if already present (PostgreSQL CI)
|
|
try:
|
|
existing = User.query.filter_by(username='testuser').first()
|
|
if existing:
|
|
if not existing.is_active:
|
|
existing.is_active = True
|
|
db.session.commit()
|
|
db.session.refresh(existing)
|
|
return existing
|
|
except Exception:
|
|
# Tables don't exist yet or other DB error, rollback and proceed to create user
|
|
db.session.rollback()
|
|
|
|
try:
|
|
user = User(
|
|
username='testuser',
|
|
role='user',
|
|
email='testuser@example.com'
|
|
)
|
|
user.is_active = True # Set after creation
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
|
|
# Refresh to ensure all relationships are loaded and object stays in session
|
|
db.session.refresh(user)
|
|
return user
|
|
except Exception:
|
|
# If tables still don't exist, try to create them
|
|
db.session.rollback()
|
|
db.create_all()
|
|
|
|
# Try again after creating tables
|
|
user = User(
|
|
username='testuser',
|
|
role='user',
|
|
email='testuser@example.com'
|
|
)
|
|
user.is_active = True # Set after creation
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(user)
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_user(app):
|
|
"""Create an admin test user."""
|
|
# Idempotent: return existing admin user if already present (PostgreSQL CI)
|
|
try:
|
|
existing = User.query.filter_by(username='admin').first()
|
|
if existing:
|
|
if existing.role != 'admin':
|
|
existing.role = 'admin'
|
|
existing.is_active = True
|
|
db.session.commit()
|
|
db.session.refresh(existing)
|
|
return existing
|
|
except Exception:
|
|
# Tables don't exist yet or other DB error, rollback and proceed to create admin
|
|
db.session.rollback()
|
|
|
|
try:
|
|
admin = User(
|
|
username='admin',
|
|
role='admin',
|
|
email='admin@example.com'
|
|
)
|
|
admin.is_active = True # Set after creation
|
|
db.session.add(admin)
|
|
db.session.commit()
|
|
|
|
# Refresh to ensure all relationships are loaded and object stays in session
|
|
db.session.refresh(admin)
|
|
return admin
|
|
except Exception:
|
|
# If tables still don't exist, try to create them
|
|
db.session.rollback()
|
|
db.create_all()
|
|
|
|
# Try again after creating tables
|
|
admin = User(
|
|
username='admin',
|
|
role='admin',
|
|
email='admin@example.com'
|
|
)
|
|
admin.is_active = True # Set after creation
|
|
db.session.add(admin)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(admin)
|
|
return admin
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_user(user):
|
|
"""Alias for user fixture (for backward compatibility with older tests)."""
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def multiple_users(app):
|
|
"""Create multiple test users."""
|
|
users = []
|
|
for i in range(1, 4):
|
|
user = User(username=f'user{i}', role='user', email=f'user{i}@example.com')
|
|
user.is_active = True # Set after creation
|
|
users.append(user)
|
|
db.session.add_all(users)
|
|
db.session.commit()
|
|
|
|
for user in users:
|
|
db.session.refresh(user)
|
|
|
|
return users
|
|
|
|
|
|
# ============================================================================
|
|
# Client Fixtures
|
|
# ============================================================================
|
|
|
|
@pytest.fixture
|
|
def test_client(app, user):
|
|
"""Create a test client (business client, not test client)."""
|
|
client = Client(
|
|
name='Test Client Corp',
|
|
description='Test client for integration tests',
|
|
contact_person='John Doe',
|
|
email='john@testclient.com',
|
|
phone='+1 (555) 123-4567',
|
|
address='123 Test Street, Test City, TC 12345',
|
|
default_hourly_rate=Decimal('85.00')
|
|
)
|
|
client.status = 'active' # Set after creation
|
|
db.session.add(client)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(client)
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def multiple_clients(app, user):
|
|
"""Create multiple test clients."""
|
|
clients = []
|
|
for i in range(1, 4):
|
|
client = Client(
|
|
name=f'Client {i}',
|
|
email=f'client{i}@example.com',
|
|
default_hourly_rate=Decimal('75.00') + Decimal(i * 10)
|
|
)
|
|
client.status = 'active' # Set after creation
|
|
clients.append(client)
|
|
db.session.add_all(clients)
|
|
db.session.commit()
|
|
|
|
for client in clients:
|
|
db.session.refresh(client)
|
|
|
|
return clients
|
|
|
|
|
|
# ============================================================================
|
|
# Project Fixtures
|
|
# ============================================================================
|
|
|
|
@pytest.fixture
|
|
def project(app, test_client):
|
|
"""Create a test project."""
|
|
project = Project(
|
|
name='Test Project',
|
|
client_id=test_client.id,
|
|
description='Test project description',
|
|
billable=True,
|
|
hourly_rate=Decimal('75.00')
|
|
)
|
|
project.status = 'active' # Set after creation
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(project)
|
|
return project
|
|
|
|
|
|
@pytest.fixture
|
|
def multiple_projects(app, test_client):
|
|
"""Create multiple test projects."""
|
|
projects = []
|
|
for i in range(1, 4):
|
|
project = Project(
|
|
name=f'Project {i}',
|
|
client_id=test_client.id,
|
|
description=f'Test project {i}',
|
|
billable=True,
|
|
hourly_rate=Decimal('75.00')
|
|
)
|
|
project.status = 'active' # Set after creation
|
|
projects.append(project)
|
|
db.session.add_all(projects)
|
|
db.session.commit()
|
|
|
|
for proj in projects:
|
|
db.session.refresh(proj)
|
|
|
|
return projects
|
|
|
|
|
|
# ============================================================================
|
|
# Time Entry Fixtures
|
|
# ============================================================================
|
|
|
|
@pytest.fixture
|
|
def time_entry(app, user, project):
|
|
"""Create a single time entry."""
|
|
start_time = datetime.utcnow() - timedelta(hours=2)
|
|
end_time = datetime.utcnow()
|
|
|
|
entry = TimeEntry(
|
|
user_id=user.id,
|
|
project_id=project.id,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
notes='Test time entry',
|
|
tags='test,development',
|
|
source='manual',
|
|
billable=True
|
|
)
|
|
db.session.add(entry)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(entry)
|
|
return entry
|
|
|
|
|
|
@pytest.fixture
|
|
def multiple_time_entries(app, user, project):
|
|
"""Create multiple time entries."""
|
|
base_time = datetime.utcnow() - timedelta(days=7)
|
|
entries = []
|
|
|
|
for i in range(5):
|
|
start = base_time + timedelta(days=i, hours=9)
|
|
end = base_time + timedelta(days=i, hours=17)
|
|
|
|
entry = TimeEntry(
|
|
user_id=user.id,
|
|
project_id=project.id,
|
|
start_time=start,
|
|
end_time=end,
|
|
notes=f'Work day {i+1}',
|
|
tags='development,testing',
|
|
source='manual',
|
|
billable=True
|
|
)
|
|
entries.append(entry)
|
|
|
|
db.session.add_all(entries)
|
|
db.session.commit()
|
|
|
|
for entry in entries:
|
|
db.session.refresh(entry)
|
|
|
|
return entries
|
|
|
|
|
|
@pytest.fixture
|
|
def active_timer(app, user, project):
|
|
"""Create an active timer (time entry without end time)."""
|
|
timer = TimeEntry(
|
|
user_id=user.id,
|
|
project_id=project.id,
|
|
start_time=datetime.utcnow(),
|
|
notes='Active timer',
|
|
source='auto',
|
|
billable=True
|
|
)
|
|
db.session.add(timer)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(timer)
|
|
return timer
|
|
|
|
|
|
# ============================================================================
|
|
# Task Fixtures
|
|
# ============================================================================
|
|
|
|
@pytest.fixture
|
|
def task(app, project, user):
|
|
"""Create a test task."""
|
|
task = Task(
|
|
name='Test Task',
|
|
description='Test task description',
|
|
project_id=project.id,
|
|
priority='medium',
|
|
created_by=user.id
|
|
)
|
|
task.status = 'todo' # Set after creation
|
|
db.session.add(task)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(task)
|
|
return task
|
|
|
|
|
|
# ============================================================================
|
|
# Invoice Fixtures
|
|
# ============================================================================
|
|
|
|
@pytest.fixture
|
|
def invoice(app, user, project, test_client):
|
|
"""Create a test invoice."""
|
|
from datetime import date
|
|
|
|
invoice = Invoice(
|
|
invoice_number=Invoice.generate_invoice_number(),
|
|
project_id=project.id,
|
|
client_id=test_client.id,
|
|
client_name=test_client.name,
|
|
due_date=date.today() + timedelta(days=30),
|
|
created_by=user.id,
|
|
tax_rate=Decimal('20.00')
|
|
)
|
|
invoice.status = 'draft' # Set after creation
|
|
db.session.add(invoice)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(invoice)
|
|
return invoice
|
|
|
|
|
|
@pytest.fixture
|
|
def invoice_with_items(app, invoice):
|
|
"""Create an invoice with items."""
|
|
items = [
|
|
InvoiceItem(
|
|
invoice_id=invoice.id,
|
|
description='Development work',
|
|
quantity=Decimal('10.00'),
|
|
unit_price=Decimal('75.00')
|
|
),
|
|
InvoiceItem(
|
|
invoice_id=invoice.id,
|
|
description='Testing work',
|
|
quantity=Decimal('5.00'),
|
|
unit_price=Decimal('60.00')
|
|
)
|
|
]
|
|
|
|
db.session.add_all(items)
|
|
db.session.commit()
|
|
|
|
invoice.calculate_totals()
|
|
db.session.commit()
|
|
|
|
db.session.refresh(invoice)
|
|
for item in items:
|
|
db.session.refresh(item)
|
|
|
|
return invoice, items
|
|
|
|
|
|
# ============================================================================
|
|
# Authentication Fixtures
|
|
# ============================================================================
|
|
|
|
@pytest.fixture
|
|
def authenticated_client(client, user):
|
|
"""Create an authenticated test client."""
|
|
with client.session_transaction() as sess:
|
|
sess['_user_id'] = str(user.id)
|
|
sess['_fresh'] = True
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_authenticated_client(client, admin_user):
|
|
"""Create an authenticated admin test client."""
|
|
with client.session_transaction() as sess:
|
|
sess['_user_id'] = str(admin_user.id)
|
|
sess['_fresh'] = True
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_headers(user):
|
|
"""Create authentication headers for API tests (session-based)."""
|
|
# Note: For tests that use headers, they should use authenticated_client instead
|
|
# This fixture is here for backward compatibility
|
|
return {}
|
|
|
|
|
|
@pytest.fixture
|
|
def regular_user(user):
|
|
"""Alias for user fixture (regular non-admin user)."""
|
|
return user
|
|
|
|
|
|
# ============================================================================
|
|
# Utility Fixtures
|
|
# ============================================================================
|
|
|
|
@pytest.fixture
|
|
def temp_file():
|
|
"""Create a temporary file for testing."""
|
|
fd, path = tempfile.mkstemp()
|
|
yield path
|
|
os.close(fd)
|
|
os.unlink(path)
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_dir():
|
|
"""Create a temporary directory for testing."""
|
|
dirpath = tempfile.mkdtemp()
|
|
yield dirpath
|
|
import shutil
|
|
shutil.rmtree(dirpath)
|
|
|
|
|
|
# ============================================================================
|
|
# Alias Fixtures (for compatibility with different test naming conventions)
|
|
# ============================================================================
|
|
|
|
@pytest.fixture
|
|
def test_client_obj(test_client):
|
|
"""Alias for test_client to avoid naming conflicts"""
|
|
return test_client
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_user(user):
|
|
"""Alias for user fixture"""
|
|
return user
|
|
|
|
|
|
@pytest.fixture
|
|
def test_project(project):
|
|
"""Alias for project fixture"""
|
|
return project
|
|
|
|
|
|
@pytest.fixture
|
|
def test_task(task):
|
|
"""Alias for task fixture"""
|
|
return task
|
|
|
|
|
|
# ============================================================================
|
|
# Installation Config Fixture
|
|
# ============================================================================
|
|
|
|
@pytest.fixture
|
|
def installation_config(temp_dir):
|
|
"""Create a temporary installation config for testing"""
|
|
from app.utils.installation import InstallationConfig
|
|
|
|
# Override the config directory to use temp directory
|
|
original_config_dir = InstallationConfig.CONFIG_DIR
|
|
InstallationConfig.CONFIG_DIR = temp_dir
|
|
|
|
# Create the config instance
|
|
config = InstallationConfig()
|
|
|
|
yield config
|
|
|
|
# Restore original config directory
|
|
InstallationConfig.CONFIG_DIR = original_config_dir
|
|
|
|
|
|
# ============================================================================
|
|
# Pytest Markers
|
|
# ============================================================================
|
|
|
|
def pytest_configure(config):
|
|
"""Configure custom pytest markers."""
|
|
config.addinivalue_line("markers", "smoke: Quick smoke tests")
|
|
config.addinivalue_line("markers", "unit: Unit tests")
|
|
config.addinivalue_line("markers", "integration: Integration tests")
|
|
config.addinivalue_line("markers", "api: API endpoint tests")
|
|
config.addinivalue_line("markers", "database: Database-related tests")
|
|
config.addinivalue_line("markers", "models: Model tests")
|
|
config.addinivalue_line("markers", "routes: Route tests")
|
|
config.addinivalue_line("markers", "security: Security tests")
|
|
config.addinivalue_line("markers", "performance: Performance tests")
|
|
config.addinivalue_line("markers", "slow: Slow running tests")
|
|
|