mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-04 18:51:53 -06:00
574 lines
16 KiB
Python
574 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',
|
|
}
|
|
|
|
|
|
@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")
|
|
|