mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-04 10:40:23 -06:00
516 lines
15 KiB
Python
516 lines
15 KiB
Python
"""
|
|
Comprehensive model testing suite.
|
|
Tests all models, relationships, properties, and business logic.
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime, timedelta, date
|
|
from decimal import Decimal
|
|
|
|
from app.models import (
|
|
User, Project, TimeEntry, Client, Settings,
|
|
Invoice, InvoiceItem, Task
|
|
)
|
|
from app import db
|
|
|
|
|
|
# ============================================================================
|
|
# User Model Tests
|
|
# ============================================================================
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
@pytest.mark.smoke
|
|
def test_user_creation(app, user):
|
|
"""Test basic user creation."""
|
|
assert user.id is not None
|
|
assert user.username == 'testuser'
|
|
assert user.role == 'user'
|
|
assert user.is_active is True
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_user_is_admin_property(app, admin_user):
|
|
"""Test user is_admin property."""
|
|
assert admin_user.is_admin is True
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_user_active_timer(app, user, active_timer):
|
|
"""Test user active_timer property."""
|
|
# Refresh user to load relationships
|
|
db.session.refresh(user)
|
|
assert user.active_timer is not None
|
|
assert user.active_timer.id == active_timer.id
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_user_time_entries_relationship(app, user, multiple_time_entries):
|
|
"""Test user time entries relationship."""
|
|
db.session.refresh(user)
|
|
assert len(user.time_entries.all()) == 5
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_user_to_dict(app, user):
|
|
"""Test user serialization to dictionary."""
|
|
user_dict = user.to_dict()
|
|
assert 'id' in user_dict
|
|
assert 'username' in user_dict
|
|
assert 'role' in user_dict
|
|
# Should not include sensitive data
|
|
assert 'password' not in user_dict
|
|
|
|
|
|
# ============================================================================
|
|
# Client Model Tests
|
|
# ============================================================================
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
@pytest.mark.smoke
|
|
def test_client_creation(app, test_client):
|
|
"""Test basic client creation."""
|
|
assert test_client.id is not None
|
|
assert test_client.name == 'Test Client Corp'
|
|
assert test_client.status == 'active'
|
|
assert test_client.default_hourly_rate == Decimal('85.00')
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_client_projects_relationship(app, test_client, multiple_projects):
|
|
"""Test client projects relationship."""
|
|
db.session.refresh(test_client)
|
|
assert len(test_client.projects.all()) == 3
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_client_total_projects_property(app, test_client, multiple_projects):
|
|
"""Test client total_projects property."""
|
|
db.session.refresh(test_client)
|
|
assert test_client.total_projects == 3
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_client_archive_activate(app, test_client):
|
|
"""Test client archive and activate methods."""
|
|
db.session.refresh(test_client)
|
|
|
|
# Archive client
|
|
test_client.archive()
|
|
db.session.commit()
|
|
assert test_client.status == 'inactive'
|
|
|
|
# Activate client
|
|
test_client.activate()
|
|
db.session.commit()
|
|
assert test_client.status == 'active'
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_client_get_active_clients(app, multiple_clients):
|
|
"""Test get_active_clients class method."""
|
|
active_clients = Client.get_active_clients()
|
|
assert len(active_clients) >= 3
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_client_to_dict(app, test_client):
|
|
"""Test client serialization to dictionary."""
|
|
client_dict = test_client.to_dict()
|
|
assert 'id' in client_dict
|
|
assert 'name' in client_dict
|
|
assert 'status' in client_dict
|
|
|
|
|
|
# ============================================================================
|
|
# Project Model Tests
|
|
# ============================================================================
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
@pytest.mark.smoke
|
|
def test_project_creation(app, project):
|
|
"""Test basic project creation."""
|
|
assert project.id is not None
|
|
assert project.name == 'Test Project'
|
|
assert project.billable is True
|
|
assert project.status == 'active'
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_project_client_relationship(app, project, test_client):
|
|
"""Test project client relationship."""
|
|
db.session.refresh(project)
|
|
db.session.refresh(test_client)
|
|
assert project.client_id == test_client.id
|
|
# Check backward compatibility
|
|
if hasattr(project, 'client'):
|
|
assert project.client == test_client.name
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_project_time_entries_relationship(app, project, multiple_time_entries):
|
|
"""Test project time entries relationship."""
|
|
db.session.refresh(project)
|
|
assert len(project.time_entries.all()) == 5
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_project_total_hours(app, project, multiple_time_entries):
|
|
"""Test project total_hours property."""
|
|
db.session.refresh(project)
|
|
# Each entry is 8 hours (9am to 5pm), 5 entries = 40 hours
|
|
assert project.total_hours > 0
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_project_estimated_cost(app, project, multiple_time_entries):
|
|
"""Test project estimated_cost property."""
|
|
db.session.refresh(project)
|
|
estimated_cost = project.estimated_cost
|
|
assert estimated_cost > 0
|
|
# Cost should be hours * hourly_rate
|
|
expected_cost = project.total_hours * float(project.hourly_rate)
|
|
assert abs(float(estimated_cost) - expected_cost) < 0.01
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_project_archive(app, project):
|
|
"""Test project archiving."""
|
|
db.session.refresh(project)
|
|
project.status = 'archived'
|
|
db.session.commit()
|
|
assert project.status == 'archived'
|
|
|
|
|
|
# ============================================================================
|
|
# Time Entry Model Tests
|
|
# ============================================================================
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
@pytest.mark.smoke
|
|
def test_time_entry_creation(app, time_entry):
|
|
"""Test basic time entry creation."""
|
|
assert time_entry.id is not None
|
|
assert time_entry.start_time is not None
|
|
assert time_entry.end_time is not None
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_time_entry_duration(app, time_entry):
|
|
"""Test time entry duration calculations."""
|
|
db.session.refresh(time_entry)
|
|
assert time_entry.duration_seconds > 0
|
|
assert time_entry.duration_hours > 0
|
|
assert time_entry.duration_formatted is not None
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_active_timer_is_active(app, active_timer):
|
|
"""Test active timer is_active property."""
|
|
db.session.refresh(active_timer)
|
|
assert active_timer.is_active is True
|
|
assert active_timer.end_time is None
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_stop_timer(app, active_timer):
|
|
"""Test stopping an active timer."""
|
|
db.session.refresh(active_timer)
|
|
active_timer.stop_timer()
|
|
db.session.commit()
|
|
|
|
db.session.refresh(active_timer)
|
|
assert active_timer.is_active is False
|
|
assert active_timer.end_time is not None
|
|
assert active_timer.duration_seconds > 0
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_time_entry_tag_list(app, test_client):
|
|
"""Test time entry tag_list property."""
|
|
from app.models import User, Project
|
|
|
|
user = User.query.first() or User(username='test', role='user')
|
|
project = Project.query.first() or Project(name='Test', client_id=test_client.id, billable=True)
|
|
|
|
if not user.id:
|
|
db.session.add(user)
|
|
if not project.id:
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
|
|
entry = TimeEntry(
|
|
user_id=user.id,
|
|
project_id=project.id,
|
|
start_time=datetime.utcnow(),
|
|
end_time=datetime.utcnow() + timedelta(hours=1),
|
|
tags='python,testing,development',
|
|
source='manual'
|
|
)
|
|
db.session.add(entry)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(entry)
|
|
assert entry.tag_list == ['python', 'testing', 'development']
|
|
|
|
|
|
# ============================================================================
|
|
# Task Model Tests
|
|
# ============================================================================
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_task_creation(app, task):
|
|
"""Test basic task creation."""
|
|
db.session.refresh(task)
|
|
assert task.id is not None
|
|
assert task.name == 'Test Task'
|
|
assert task.status == 'todo'
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_task_project_relationship(app, task, project):
|
|
"""Test task project relationship."""
|
|
db.session.refresh(task)
|
|
db.session.refresh(project)
|
|
assert task.project_id == project.id
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_task_status_transitions(app, task):
|
|
"""Test task status transitions."""
|
|
db.session.refresh(task)
|
|
|
|
# Mark as in progress
|
|
task.status = 'in_progress'
|
|
db.session.commit()
|
|
assert task.status == 'in_progress'
|
|
|
|
# Mark as done
|
|
task.status = 'done'
|
|
db.session.commit()
|
|
assert task.status == 'done'
|
|
|
|
|
|
# ============================================================================
|
|
# Invoice Model Tests
|
|
# ============================================================================
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
@pytest.mark.smoke
|
|
def test_invoice_creation(app, invoice):
|
|
"""Test basic invoice creation."""
|
|
# Invoice is already refreshed in the fixture, no need to refresh again
|
|
assert invoice.id is not None
|
|
assert invoice.invoice_number is not None
|
|
assert invoice.status == 'draft'
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_invoice_number_generation(app):
|
|
"""Test invoice number generation."""
|
|
invoice_number = Invoice.generate_invoice_number()
|
|
assert invoice_number is not None
|
|
assert 'INV-' in invoice_number
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_invoice_calculate_totals(app, invoice_with_items):
|
|
"""Test invoice total calculations."""
|
|
invoice, items = invoice_with_items
|
|
|
|
# Invoice is already committed and refreshed in the fixture
|
|
# 10 * 75 + 5 * 60 = 750 + 300 = 1050
|
|
assert invoice.subtotal == Decimal('1050.00')
|
|
|
|
# Tax: 20% of 1050 = 210
|
|
assert invoice.tax_amount == Decimal('210.00')
|
|
|
|
# Total: 1050 + 210 = 1260
|
|
assert invoice.total_amount == Decimal('1260.00')
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_invoice_payment_tracking(app, invoice_with_items):
|
|
"""Test invoice payment tracking."""
|
|
invoice, items = invoice_with_items
|
|
|
|
# Record partial payment
|
|
partial_payment = invoice.total_amount / 2
|
|
invoice.record_payment(
|
|
amount=partial_payment,
|
|
payment_date=date.today(),
|
|
payment_method='bank_transfer',
|
|
payment_reference='TEST-123'
|
|
)
|
|
db.session.commit()
|
|
|
|
db.session.expire(invoice)
|
|
db.session.refresh(invoice)
|
|
assert invoice.payment_status == 'partially_paid'
|
|
assert invoice.amount_paid == partial_payment
|
|
assert invoice.is_partially_paid is True
|
|
|
|
# Record remaining payment
|
|
remaining = invoice.outstanding_amount
|
|
invoice.record_payment(
|
|
amount=remaining,
|
|
payment_method='bank_transfer'
|
|
)
|
|
db.session.commit()
|
|
|
|
db.session.expire(invoice)
|
|
db.session.refresh(invoice)
|
|
assert invoice.payment_status == 'fully_paid'
|
|
assert invoice.is_paid is True
|
|
assert invoice.outstanding_amount == Decimal('0')
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_invoice_overdue_status(app, user, project, test_client):
|
|
"""Test invoice overdue status."""
|
|
# Create overdue invoice
|
|
overdue_invoice = Invoice(
|
|
invoice_number=Invoice.generate_invoice_number(),
|
|
project_id=project.id,
|
|
client_id=test_client.id,
|
|
client_name='Test Client',
|
|
due_date=date.today() - timedelta(days=10),
|
|
created_by=user.id
|
|
)
|
|
# Set status after creation (not in __init__)
|
|
overdue_invoice.status = 'sent'
|
|
db.session.add(overdue_invoice)
|
|
db.session.commit()
|
|
|
|
db.session.refresh(overdue_invoice)
|
|
assert overdue_invoice.is_overdue is True
|
|
assert overdue_invoice.days_overdue == 10
|
|
|
|
|
|
# ============================================================================
|
|
# Settings Model Tests
|
|
# ============================================================================
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_settings_singleton(app):
|
|
"""Test settings singleton pattern."""
|
|
settings1 = Settings.get_settings()
|
|
settings2 = Settings.get_settings()
|
|
|
|
assert settings1.id == settings2.id
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_settings_default_values(app):
|
|
"""Test settings default values."""
|
|
settings = Settings.get_settings()
|
|
|
|
# Check that settings has expected attributes
|
|
assert hasattr(settings, 'id')
|
|
# Add more default value checks based on your Settings model
|
|
|
|
|
|
# ============================================================================
|
|
# Model Relationship Tests
|
|
# ============================================================================
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.models
|
|
@pytest.mark.database
|
|
def test_cascade_delete_user_time_entries(app, user, multiple_time_entries):
|
|
"""Test cascade delete of user time entries."""
|
|
user_id = user.id
|
|
|
|
# Get time entry count
|
|
entry_count = TimeEntry.query.filter_by(user_id=user_id).count()
|
|
assert entry_count == 5
|
|
|
|
# Delete user
|
|
db.session.delete(user)
|
|
db.session.commit()
|
|
|
|
# Check time entries are deleted or handled
|
|
remaining_entries = TimeEntry.query.filter_by(user_id=user_id).count()
|
|
# Depending on cascade settings, entries might be deleted or set to null
|
|
# For now, we just verify the operation completed without errors
|
|
assert remaining_entries >= 0 # Operation completed successfully
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.models
|
|
@pytest.mark.database
|
|
def test_project_client_relationship_integrity(app, project, test_client):
|
|
"""Test project-client relationship integrity."""
|
|
# Verify the relationship
|
|
assert project.client_id == test_client.id
|
|
|
|
# Get project through client relationship
|
|
client_projects = Client.query.get(test_client.id).projects.all()
|
|
project_ids = [p.id for p in client_projects]
|
|
assert project.id in project_ids
|
|
|
|
|
|
# ============================================================================
|
|
# Model Validation Tests
|
|
# ============================================================================
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_project_requires_name(app, test_client):
|
|
"""Test that project requires a name."""
|
|
# Project __init__ requires name as first positional argument
|
|
# This test verifies the API enforces this requirement
|
|
with pytest.raises(TypeError):
|
|
project = Project(billable=True)
|
|
|
|
|
|
@pytest.mark.unit
|
|
@pytest.mark.models
|
|
def test_time_entry_requires_start_time(app, user, project):
|
|
"""Test that time entry requires start time."""
|
|
# TimeEntry requires start_time at database level (nullable=False)
|
|
# This test verifies the database enforces this requirement
|
|
from sqlalchemy.exc import IntegrityError
|
|
from app import db
|
|
|
|
with pytest.raises(IntegrityError):
|
|
entry = TimeEntry(
|
|
user_id=user.id,
|
|
project_id=project.id,
|
|
source='manual'
|
|
)
|
|
db.session.add(entry)
|
|
db.session.commit()
|
|
|