Files
TimeTracker/tests/test_models_extended.py
Dries Peeters 113a57d2eb testing updates
2025-10-10 11:37:23 +02:00

717 lines
21 KiB
Python

"""Extended model tests for additional coverage"""
import pytest
from datetime import datetime, timedelta
from decimal import Decimal
from app import db
from app.models import (
User, Client, Project, TimeEntry, Invoice, InvoiceItem,
Task, Comment, Settings
)
# ============================================================================
# User Model Extended Tests
# ============================================================================
@pytest.mark.unit
@pytest.mark.models
def test_user_display_name(app):
"""Test user display name property"""
with app.app_context():
# User with full name
user1 = User(username='testuser', email='test@example.com', full_name='Test User')
assert user1.display_name == 'Test User'
# User without full name
user2 = User(username='anotheruser', email='another@example.com')
assert user2.display_name == 'anotheruser'
@pytest.mark.unit
@pytest.mark.models
def test_user_total_hours(user):
"""Test user total hours calculation"""
# Should return 0 or a number >= 0
assert user.total_hours >= 0
@pytest.mark.unit
@pytest.mark.models
def test_user_repr(user):
"""Test user repr"""
assert repr(user) == f'<User {user.username}>'
@pytest.mark.unit
@pytest.mark.models
def test_user_projects_through_time_entries(app, user, project):
"""Test getting user's projects through time entries"""
with app.app_context():
user = db.session.merge(user)
project = db.session.merge(project)
# Create time entry
entry = TimeEntry(
user_id=user.id,
project_id=project.id,
start_time=datetime.utcnow(),
end_time=datetime.utcnow() + timedelta(hours=2),
source='manual'
)
db.session.add(entry)
db.session.commit()
# Get user's projects
projects = set(entry.project for entry in user.time_entries.all())
assert project in projects
# ============================================================================
# Client Model Extended Tests
# ============================================================================
@pytest.mark.unit
@pytest.mark.models
def test_client_status_property(test_client):
"""Test client status and is_active property"""
assert test_client.status in ['active', 'inactive']
if test_client.status == 'active':
assert test_client.is_active
@pytest.mark.unit
@pytest.mark.models
def test_client_repr(test_client):
"""Test client repr"""
assert repr(test_client) == f'<Client {test_client.name}>'
@pytest.mark.unit
@pytest.mark.models
def test_client_with_multiple_projects(app, test_client):
"""Test client with multiple projects"""
with app.app_context():
test_client = db.session.merge(test_client)
# Create multiple projects
for i in range(5):
project = Project(
name=f'Project {i}',
client_id=test_client.id,
billable=True,
hourly_rate=100.0
)
db.session.add(project)
db.session.commit()
db.session.refresh(test_client)
assert test_client.total_projects >= 5
@pytest.mark.unit
@pytest.mark.models
def test_client_archive_activate_methods(app, test_client):
"""Test client archive and activate methods"""
with app.app_context():
test_client = db.session.merge(test_client)
# Initially should be active
initial_status = test_client.status
assert initial_status == 'active'
# Archive the client
test_client.archive()
db.session.commit()
assert test_client.status == 'inactive'
assert not test_client.is_active
# Activate the client
test_client.activate()
db.session.commit()
assert test_client.status == 'active'
assert test_client.is_active
# ============================================================================
# Project Model Extended Tests
# ============================================================================
@pytest.mark.unit
@pytest.mark.models
def test_project_status(project):
"""Test project status"""
assert project.status in ['active', 'inactive', 'completed']
assert hasattr(project, 'is_active')
@pytest.mark.unit
@pytest.mark.models
def test_project_billable_hours(project):
"""Test project billable hours calculation"""
# Should return 0 or a number >= 0
if hasattr(project, 'total_billable_hours'):
assert project.total_billable_hours >= 0
@pytest.mark.unit
@pytest.mark.models
def test_project_with_no_time_entries(app, test_client):
"""Test project total hours with no time entries"""
with app.app_context():
test_client = db.session.merge(test_client)
project = Project(
name='Empty Project',
client_id=test_client.id,
billable=True,
hourly_rate=100.0
)
db.session.add(project)
db.session.commit()
assert project.total_hours == 0.0
@pytest.mark.unit
@pytest.mark.models
def test_project_hourly_rate(app, test_client):
"""Test project hourly rate"""
with app.app_context():
test_client = db.session.merge(test_client)
project = Project(
name='Cost Project',
client_id=test_client.id,
billable=True,
hourly_rate=100.0
)
db.session.add(project)
db.session.commit()
assert project.hourly_rate == 100.0
assert project.billable
@pytest.mark.unit
@pytest.mark.models
def test_project_non_billable(app, test_client):
"""Test non-billable project"""
with app.app_context():
test_client = db.session.merge(test_client)
project = Project(
name='Non-Billable Project',
client_id=test_client.id,
billable=False
)
db.session.add(project)
db.session.commit()
assert not project.billable
assert project.hourly_rate == 0.0 or project.hourly_rate is None
@pytest.mark.unit
@pytest.mark.models
def test_project_to_dict(app, project):
"""Test project to_dict method"""
with app.app_context():
project = db.session.merge(project)
project_dict = project.to_dict()
assert 'id' in project_dict
assert 'name' in project_dict
# Project may use 'client' key instead of 'client_id'
assert 'client' in project_dict or 'client_id' in project_dict
assert project_dict['name'] == project.name
# ============================================================================
# TimeEntry Model Extended Tests
# ============================================================================
@pytest.mark.unit
@pytest.mark.models
def test_time_entry_str_representation(time_entry):
"""Test time entry string representation"""
str_repr = str(time_entry)
assert 'TimeEntry' in str_repr
@pytest.mark.unit
@pytest.mark.models
def test_time_entry_with_notes(app, user, project):
"""Test time entry with notes"""
with app.app_context():
user = db.session.merge(user)
project = db.session.merge(project)
notes = "Worked on implementing new feature X"
entry = TimeEntry(
user_id=user.id,
project_id=project.id,
start_time=datetime.utcnow(),
end_time=datetime.utcnow() + timedelta(hours=2),
notes=notes,
source='manual'
)
db.session.add(entry)
db.session.commit()
assert entry.notes == notes
@pytest.mark.unit
@pytest.mark.models
def test_time_entry_with_tags(app, user, project):
"""Test time entry with tags"""
with app.app_context():
user = db.session.merge(user)
project = db.session.merge(project)
entry = TimeEntry(
user_id=user.id,
project_id=project.id,
start_time=datetime.utcnow(),
end_time=datetime.utcnow() + timedelta(hours=2),
tags='development,testing,bugfix',
source='manual'
)
db.session.add(entry)
db.session.commit()
tag_list = entry.tag_list
assert 'development' in tag_list
assert 'testing' in tag_list
assert 'bugfix' in tag_list
@pytest.mark.unit
@pytest.mark.models
def test_time_entry_billable_calculation(app, user, project):
"""Test time entry billable cost calculation"""
with app.app_context():
user = db.session.merge(user)
project = db.session.merge(project)
project.billable = True
project.hourly_rate = 100.0
entry = TimeEntry(
user_id=user.id,
project_id=project.id,
start_time=datetime.utcnow(),
end_time=datetime.utcnow() + timedelta(hours=3),
source='manual'
)
db.session.add(entry)
db.session.commit()
# 3 hours * $100/hr = $300
expected_cost = 3.0 * 100.0
if hasattr(entry, 'billable_amount'):
assert entry.billable_amount == expected_cost
@pytest.mark.unit
@pytest.mark.models
def test_time_entry_long_duration(app, user, project):
"""Test time entry with very long duration"""
with app.app_context():
user = db.session.merge(user)
project = db.session.merge(project)
start = datetime.utcnow()
end = start + timedelta(hours=24) # 24 hours
entry = TimeEntry(
user_id=user.id,
project_id=project.id,
start_time=start,
end_time=end,
source='manual'
)
db.session.add(entry)
db.session.commit()
# Check duration through time difference
duration_seconds = (end - start).total_seconds()
assert duration_seconds >= 24 * 3600
# ============================================================================
# Task Model Extended Tests
# ============================================================================
@pytest.mark.unit
@pytest.mark.models
def test_task_str_representation(task):
"""Test task string representation"""
str_repr = str(task)
assert 'Task' in str_repr or task.name in str_repr
@pytest.mark.unit
@pytest.mark.models
def test_task_repr(task):
"""Test task repr"""
repr_str = repr(task)
assert 'Task' in repr_str
@pytest.mark.unit
@pytest.mark.models
def test_task_with_priority(app, project, user):
"""Test task with priority levels"""
with app.app_context():
project = db.session.merge(project)
user = db.session.merge(user)
for priority in ['low', 'medium', 'high']:
task = Task(
project_id=project.id,
name=f'Task with {priority} priority',
assigned_to=user.id,
created_by=user.id,
priority=priority
)
db.session.add(task)
db.session.commit()
# Verify tasks were created
tasks = Task.query.filter_by(project_id=project.id).all()
assert len(tasks) >= 3
@pytest.mark.unit
@pytest.mark.models
def test_task_with_due_date(app, project, user):
"""Test task with due date"""
with app.app_context():
project = db.session.merge(project)
user = db.session.merge(user)
due_date = datetime.utcnow() + timedelta(days=7)
task = Task(
project_id=project.id,
name='Task with deadline',
assigned_to=user.id,
created_by=user.id,
due_date=due_date
)
db.session.add(task)
db.session.commit()
# Verify task was created
assert task.id is not None
if hasattr(task, 'due_date'):
assert task.due_date is not None
@pytest.mark.unit
@pytest.mark.models
def test_task_completion(app, task):
"""Test marking task as completed"""
with app.app_context():
task = db.session.merge(task)
task.status = 'completed'
task.completed_at = datetime.utcnow()
db.session.commit()
assert task.status == 'completed'
if hasattr(task, 'completed_at'):
assert task.completed_at is not None
# ============================================================================
# Invoice Model Extended Tests
# ============================================================================
@pytest.mark.unit
@pytest.mark.models
def test_invoice_str_representation(invoice):
"""Test invoice string representation"""
str_repr = str(invoice)
assert 'Invoice' in str_repr or invoice.invoice_number in str_repr
@pytest.mark.unit
@pytest.mark.models
def test_invoice_repr(invoice):
"""Test invoice repr"""
repr_str = repr(invoice)
assert 'Invoice' in repr_str
@pytest.mark.unit
@pytest.mark.models
def test_invoice_with_multiple_items(app, test_client, project, user):
"""Test invoice with multiple line items"""
with app.app_context():
test_client = db.session.merge(test_client)
project = db.session.merge(project)
user = db.session.merge(user)
invoice = Invoice(
client_id=test_client.id,
project_id=project.id,
client_name=test_client.name,
invoice_number='INV-TEST-001',
issue_date=datetime.utcnow().date(),
due_date=(datetime.utcnow() + timedelta(days=30)).date(),
status='draft',
created_by=user.id
)
db.session.add(invoice)
db.session.flush()
# Add multiple items
for i in range(5):
item = InvoiceItem(
invoice_id=invoice.id,
description=f'Service {i+1}',
quantity=i+1,
unit_price=100.0
)
db.session.add(item)
db.session.commit()
db.session.refresh(invoice)
# Verify items were added
if hasattr(invoice, 'items'):
assert len(invoice.items.all()) == 5
@pytest.mark.unit
@pytest.mark.models
def test_invoice_with_discount(app, invoice):
"""Test invoice with discount applied"""
with app.app_context():
invoice = db.session.merge(invoice)
if hasattr(invoice, 'discount'):
invoice.discount = 10.0 # 10% discount
db.session.commit()
invoice.calculate_totals()
assert invoice.total < invoice.subtotal
@pytest.mark.unit
@pytest.mark.models
def test_invoice_status_transitions(app, test_client, project, user):
"""Test invoice status transitions"""
with app.app_context():
test_client = db.session.merge(test_client)
project = db.session.merge(project)
user = db.session.merge(user)
invoice = Invoice(
client_id=test_client.id,
project_id=project.id,
client_name=test_client.name,
invoice_number='INV-STATUS-001',
issue_date=datetime.utcnow().date(),
due_date=(datetime.utcnow() + timedelta(days=30)).date(),
status='draft',
created_by=user.id
)
db.session.add(invoice)
db.session.commit()
# Test status transitions
assert invoice.status == 'draft'
invoice.status = 'sent'
db.session.commit()
assert invoice.status == 'sent'
invoice.status = 'paid'
db.session.commit()
assert invoice.status == 'paid'
# ============================================================================
# Comment Model Extended Tests
# ============================================================================
@pytest.mark.unit
@pytest.mark.models
def test_comment_creation(app, user, task):
"""Test creating a comment on a task"""
with app.app_context():
user = db.session.merge(user)
task = db.session.merge(task)
comment = Comment(
content='This is a test comment',
user_id=user.id,
task_id=task.id
)
db.session.add(comment)
db.session.commit()
assert comment.id is not None
assert comment.content == 'This is a test comment'
assert comment.task_id == task.id
assert comment.user_id == user.id
@pytest.mark.unit
@pytest.mark.models
def test_comment_str_representation(app, user, task):
"""Test comment string representation"""
with app.app_context():
user = db.session.merge(user)
task = db.session.merge(task)
comment = Comment(
content='Test comment',
user_id=user.id,
task_id=task.id
)
db.session.add(comment)
db.session.commit()
str_repr = str(comment)
assert 'Comment' in str_repr or 'Test comment' in str_repr
# ============================================================================
# Settings Model Extended Tests
# ============================================================================
@pytest.mark.unit
@pytest.mark.models
def test_settings_update(app):
"""Test updating settings"""
with app.app_context():
settings = Settings.get_settings()
original_company = settings.company_name
settings.company_name = 'Updated Company Name'
db.session.commit()
# Verify update
settings = Settings.get_settings()
assert settings.company_name == 'Updated Company Name'
assert settings.company_name != original_company
@pytest.mark.unit
@pytest.mark.models
def test_settings_currency(app):
"""Test settings currency configuration"""
with app.app_context():
settings = Settings.get_settings()
# Test different currencies
for currency in ['USD', 'EUR', 'GBP', 'JPY']:
settings.currency = currency
db.session.commit()
settings = Settings.get_settings()
assert settings.currency == currency
@pytest.mark.unit
@pytest.mark.models
def test_settings_timezone_validation(app):
"""Test that invalid timezones are handled"""
with app.app_context():
settings = Settings.get_settings()
# Set a valid timezone
settings.timezone = 'America/New_York'
db.session.commit()
settings = Settings.get_settings()
assert settings.timezone == 'America/New_York'
@pytest.mark.unit
@pytest.mark.models
def test_settings_str_representation(app):
"""Test settings string representation"""
with app.app_context():
settings = Settings.get_settings()
str_repr = str(settings)
assert 'Settings' in str_repr
# ============================================================================
# Relationship Tests
# ============================================================================
@pytest.mark.integration
@pytest.mark.models
def test_user_client_relationship_through_projects(app, user, test_client):
"""Test user-client relationship through projects and time entries"""
with app.app_context():
user = db.session.merge(user)
test_client = db.session.merge(test_client)
# Create project
project = Project(
name='Relationship Test Project',
client_id=test_client.id,
billable=True,
hourly_rate=100.0
)
db.session.add(project)
db.session.flush()
# Create time entry
entry = TimeEntry(
user_id=user.id,
project_id=project.id,
start_time=datetime.utcnow(),
end_time=datetime.utcnow() + timedelta(hours=2),
source='manual'
)
db.session.add(entry)
db.session.commit()
# Verify relationships
assert entry.project.client_id == test_client.id
assert entry.user_id == user.id
@pytest.mark.integration
@pytest.mark.models
def test_task_comment_relationship(app, user, project):
"""Test task-comment relationship"""
with app.app_context():
user = db.session.merge(user)
project = db.session.merge(project)
# Create task
task = Task(
project_id=project.id,
name='Task with comments',
assigned_to=user.id,
created_by=user.id
)
db.session.add(task)
db.session.flush()
# Add comments
for i in range(3):
comment = Comment(
content=f'Comment {i+1}',
user_id=user.id,
task_id=task.id
)
db.session.add(comment)
db.session.commit()
db.session.refresh(task)
# Verify relationship
if hasattr(task, 'comments'):
assert len(task.comments) >= 3