mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-05 11:09:55 -06:00
717 lines
21 KiB
Python
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
|
|
|