"""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 from factories import ClientFactory, ProjectFactory, InvoiceFactory, InvoiceItemFactory, UserFactory # ============================================================================ # 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"" @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 from factories import TimeEntryFactory entry = TimeEntryFactory( user_id=user.id, project_id=project.id, start_time=datetime.utcnow(), end_time=datetime.utcnow() + timedelta(hours=2), source="manual", ) 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"" @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" from factories import TimeEntryFactory entry = TimeEntryFactory( 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.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) from factories import TimeEntryFactory entry = TimeEntryFactory( 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.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 from factories import TimeEntryFactory entry = TimeEntryFactory( user_id=user.id, project_id=project.id, start_time=datetime.utcnow(), end_time=datetime.utcnow() + timedelta(hours=3), source="manual", ) 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 from factories import TimeEntryFactory entry = TimeEntryFactory( user_id=user.id, project_id=project.id, start_time=start, end_time=end, source="manual" ) 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 = InvoiceFactory( 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, ) # Add multiple items for i in range(5): InvoiceItemFactory(invoice_id=invoice.id, description=f"Service {i+1}", quantity=i + 1, unit_price=100.0) 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 = InvoiceFactory( 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.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 from factories import TimeEntryFactory entry = TimeEntryFactory( user_id=user.id, project_id=project.id, start_time=datetime.utcnow(), end_time=datetime.utcnow() + timedelta(hours=2), source="manual", ) 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