mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-06 03:30:25 -06:00
- Normalize line endings from CRLF to LF across all files to match .editorconfig - Standardize quote style from single quotes to double quotes - Normalize whitespace and formatting throughout codebase - Apply consistent code style across 372 files including: * Application code (models, routes, services, utils) * Test files * Configuration files * CI/CD workflows This ensures consistency with the project's .editorconfig settings and improves code maintainability.
494 lines
18 KiB
Python
494 lines
18 KiB
Python
"""
|
|
Comprehensive tests for Project Dashboard functionality.
|
|
|
|
This module tests:
|
|
- Project dashboard route and access
|
|
- Budget vs actual data calculations
|
|
- Task statistics aggregation
|
|
- Team member contributions
|
|
- Recent activity tracking
|
|
- Timeline data generation
|
|
- Period filtering
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import date, datetime, timedelta
|
|
from decimal import Decimal
|
|
from app import create_app, db
|
|
from app.models import User, Project, Client, Task, TimeEntry, Activity, ProjectCost
|
|
|
|
# Skip all tests in this module due to pre-existing model initialization issues
|
|
pytestmark = pytest.mark.skip(reason="Pre-existing issues with Task model initialization - needs refactoring")
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
"""Create and configure a test application instance."""
|
|
app = create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:", "WTF_CSRF_ENABLED": False})
|
|
|
|
with app.app_context():
|
|
db.create_all()
|
|
yield app
|
|
db.session.remove()
|
|
db.drop_all()
|
|
|
|
|
|
@pytest.fixture
|
|
def client_fixture(app):
|
|
"""Create a test Flask client."""
|
|
return app.test_client()
|
|
|
|
|
|
@pytest.fixture
|
|
def test_user(app):
|
|
"""Create a test user."""
|
|
with app.app_context():
|
|
user = User(username="testuser", role="user", email="test@example.com")
|
|
user.set_password("testpass123")
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user.id
|
|
|
|
|
|
@pytest.fixture
|
|
def test_user2(app):
|
|
"""Create a second test user."""
|
|
with app.app_context():
|
|
user = User(username="testuser2", role="user", email="test2@example.com", full_name="Test User 2")
|
|
user.set_password("testpass123")
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user.id
|
|
|
|
|
|
@pytest.fixture
|
|
def test_admin(app):
|
|
"""Create a test admin user."""
|
|
with app.app_context():
|
|
admin = User(username="admin", role="admin", email="admin@example.com")
|
|
admin.set_password("adminpass123")
|
|
db.session.add(admin)
|
|
db.session.commit()
|
|
return admin.id
|
|
|
|
|
|
@pytest.fixture
|
|
def test_client(app):
|
|
"""Create a test client."""
|
|
with app.app_context():
|
|
client = Client(name="Test Client", description="A test client")
|
|
db.session.add(client)
|
|
db.session.commit()
|
|
return client.id
|
|
|
|
|
|
@pytest.fixture
|
|
def test_project(app, test_client):
|
|
"""Create a test project with budget."""
|
|
with app.app_context():
|
|
project = Project(
|
|
name="Dashboard Test Project",
|
|
client_id=test_client,
|
|
description="A test project for dashboard",
|
|
billable=True,
|
|
hourly_rate=Decimal("100.00"),
|
|
budget_amount=Decimal("5000.00"),
|
|
)
|
|
project.estimated_hours = 50.0
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
return project.id
|
|
|
|
|
|
@pytest.fixture
|
|
def test_project_with_data(app, test_project, test_user, test_user2):
|
|
"""Create a test project with tasks and time entries."""
|
|
with app.app_context():
|
|
project = db.session.get(Project, test_project)
|
|
|
|
# Create tasks with different statuses
|
|
task1 = Task(
|
|
project_id=project.id,
|
|
name="Task 1 - Todo",
|
|
status="todo",
|
|
priority="high",
|
|
created_by=test_user,
|
|
assigned_to=test_user,
|
|
)
|
|
task2 = Task(
|
|
project_id=project.id,
|
|
name="Task 2 - In Progress",
|
|
status="in_progress",
|
|
priority="medium",
|
|
created_by=test_user,
|
|
assigned_to=test_user2,
|
|
)
|
|
task3 = Task(
|
|
project_id=project.id,
|
|
name="Task 3 - Done",
|
|
status="done",
|
|
priority="low",
|
|
created_by=test_user,
|
|
assigned_to=test_user,
|
|
completed_at=datetime.now(),
|
|
)
|
|
task4 = Task(
|
|
project_id=project.id,
|
|
name="Task 4 - Overdue",
|
|
status="todo",
|
|
priority="urgent",
|
|
due_date=date.today() - timedelta(days=5),
|
|
created_by=test_user,
|
|
assigned_to=test_user,
|
|
)
|
|
|
|
db.session.add_all([task1, task2, task3, task4])
|
|
|
|
# Create time entries for both users
|
|
now = datetime.now()
|
|
|
|
# User 1: 10 hours across 3 entries
|
|
entry1 = TimeEntry(
|
|
user_id=test_user,
|
|
project_id=project.id,
|
|
task_id=task1.id,
|
|
start_time=now - timedelta(days=2, hours=4),
|
|
end_time=now - timedelta(days=2),
|
|
duration_seconds=14400, # 4 hours
|
|
billable=True,
|
|
)
|
|
entry2 = TimeEntry(
|
|
user_id=test_user,
|
|
project_id=project.id,
|
|
task_id=task3.id,
|
|
start_time=now - timedelta(days=1, hours=3),
|
|
end_time=now - timedelta(days=1),
|
|
duration_seconds=10800, # 3 hours
|
|
billable=True,
|
|
)
|
|
entry3 = TimeEntry(
|
|
user_id=test_user,
|
|
project_id=project.id,
|
|
start_time=now - timedelta(hours=3),
|
|
end_time=now,
|
|
duration_seconds=10800, # 3 hours
|
|
billable=True,
|
|
)
|
|
|
|
# User 2: 5 hours across 2 entries
|
|
entry4 = TimeEntry(
|
|
user_id=test_user2,
|
|
project_id=project.id,
|
|
task_id=task2.id,
|
|
start_time=now - timedelta(days=1, hours=3),
|
|
end_time=now - timedelta(days=1),
|
|
duration_seconds=10800, # 3 hours
|
|
billable=True,
|
|
)
|
|
entry5 = TimeEntry(
|
|
user_id=test_user2,
|
|
project_id=project.id,
|
|
start_time=now - timedelta(hours=2),
|
|
end_time=now,
|
|
duration_seconds=7200, # 2 hours
|
|
billable=True,
|
|
)
|
|
|
|
db.session.add_all([entry1, entry2, entry3, entry4, entry5])
|
|
|
|
# Create some activities
|
|
Activity.log(
|
|
user_id=test_user,
|
|
action="created",
|
|
entity_type="project",
|
|
entity_id=project.id,
|
|
entity_name=project.name,
|
|
description=f'Created project "{project.name}"',
|
|
)
|
|
|
|
Activity.log(
|
|
user_id=test_user,
|
|
action="created",
|
|
entity_type="task",
|
|
entity_id=task1.id,
|
|
entity_name=task1.name,
|
|
description=f'Created task "{task1.name}"',
|
|
)
|
|
|
|
Activity.log(
|
|
user_id=test_user,
|
|
action="completed",
|
|
entity_type="task",
|
|
entity_id=task3.id,
|
|
entity_name=task3.name,
|
|
description=f'Completed task "{task3.name}"',
|
|
)
|
|
|
|
db.session.commit()
|
|
return project.id
|
|
|
|
|
|
def login(client, username="testuser", password="testpass123"):
|
|
"""Helper function to log in a user."""
|
|
return client.post("/auth/login", data={"username": username, "password": password}, follow_redirects=True)
|
|
|
|
|
|
class TestProjectDashboardAccess:
|
|
"""Tests for dashboard access and permissions."""
|
|
|
|
def test_dashboard_requires_login(self, app, client_fixture, test_project):
|
|
"""Test that dashboard requires authentication."""
|
|
with app.app_context():
|
|
response = client_fixture.get(f"/projects/{test_project}/dashboard")
|
|
assert response.status_code == 302 # Redirect to login
|
|
|
|
def test_dashboard_accessible_when_logged_in(self, app, client_fixture, test_project, test_user):
|
|
"""Test that dashboard is accessible when logged in."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project}/dashboard")
|
|
assert response.status_code == 200
|
|
|
|
def test_dashboard_404_for_nonexistent_project(self, app, client_fixture, test_user):
|
|
"""Test that dashboard returns 404 for non-existent project."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get("/projects/99999/dashboard")
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestDashboardData:
|
|
"""Tests for dashboard data calculations and aggregations."""
|
|
|
|
def test_budget_data_calculation(self, app, client_fixture, test_project_with_data, test_user):
|
|
"""Test that budget data is calculated correctly."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project_with_data}/dashboard")
|
|
assert response.status_code == 200
|
|
|
|
# Check that budget-related content is in response
|
|
assert b"Budget vs. Actual" in response.data
|
|
|
|
# Get project and verify calculations
|
|
project = db.session.get(Project, test_project_with_data)
|
|
assert project.budget_amount is not None
|
|
assert project.total_hours > 0
|
|
|
|
def test_task_statistics(self, app, client_fixture, test_project_with_data, test_user):
|
|
"""Test that task statistics are calculated correctly."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project_with_data}/dashboard")
|
|
assert response.status_code == 200
|
|
|
|
# Verify task statistics in response
|
|
assert b"Task Status Distribution" in response.data
|
|
assert b"Tasks Complete" in response.data
|
|
|
|
# Verify task counts
|
|
project = db.session.get(Project, test_project_with_data)
|
|
tasks = project.tasks.all()
|
|
assert len(tasks) == 4 # We created 4 tasks
|
|
|
|
# Check task statuses
|
|
statuses = [task.status for task in tasks]
|
|
assert "todo" in statuses
|
|
assert "in_progress" in statuses
|
|
assert "done" in statuses
|
|
|
|
def test_team_contributions(self, app, client_fixture, test_project_with_data, test_user):
|
|
"""Test that team member contributions are calculated correctly."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project_with_data}/dashboard")
|
|
assert response.status_code == 200
|
|
|
|
# Verify team contributions section exists
|
|
assert b"Team Member Contributions" in response.data
|
|
assert b"Team Members" in response.data
|
|
|
|
# Get project and verify user totals
|
|
project = db.session.get(Project, test_project_with_data)
|
|
user_totals = project.get_user_totals()
|
|
assert len(user_totals) == 2 # Two users contributed
|
|
|
|
# Verify hours distribution
|
|
total_hours = sum([ut["total_hours"] for ut in user_totals])
|
|
assert total_hours == 15.0 # 10 + 5 hours
|
|
|
|
def test_recent_activity(self, app, client_fixture, test_project_with_data, test_user):
|
|
"""Test that recent activity is displayed correctly."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project_with_data}/dashboard")
|
|
assert response.status_code == 200
|
|
|
|
# Verify recent activity section exists
|
|
assert b"Recent Activity" in response.data
|
|
|
|
# Verify activities exist in database
|
|
project = db.session.get(Project, test_project_with_data)
|
|
activities = Activity.query.filter_by(entity_type="project", entity_id=project.id).all()
|
|
assert len(activities) >= 1
|
|
|
|
def test_overdue_tasks_warning(self, app, client_fixture, test_project_with_data, test_user):
|
|
"""Test that overdue tasks trigger a warning."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project_with_data}/dashboard")
|
|
assert response.status_code == 200
|
|
|
|
# Verify overdue warning is shown
|
|
assert b"Attention Required" in response.data or b"overdue" in response.data.lower()
|
|
|
|
|
|
class TestDashboardPeriodFiltering:
|
|
"""Tests for dashboard time period filtering."""
|
|
|
|
def test_period_filter_all_time(self, app, client_fixture, test_project_with_data, test_user):
|
|
"""Test dashboard with 'all time' filter."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project_with_data}/dashboard?period=all")
|
|
assert response.status_code == 200
|
|
assert b"All Time" in response.data
|
|
|
|
def test_period_filter_week(self, app, client_fixture, test_project_with_data, test_user):
|
|
"""Test dashboard with 'last week' filter."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project_with_data}/dashboard?period=week")
|
|
assert response.status_code == 200
|
|
|
|
def test_period_filter_month(self, app, client_fixture, test_project_with_data, test_user):
|
|
"""Test dashboard with 'last month' filter."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project_with_data}/dashboard?period=month")
|
|
assert response.status_code == 200
|
|
|
|
def test_period_filter_three_months(self, app, client_fixture, test_project_with_data, test_user):
|
|
"""Test dashboard with '3 months' filter."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project_with_data}/dashboard?period=3months")
|
|
assert response.status_code == 200
|
|
|
|
def test_period_filter_year(self, app, client_fixture, test_project_with_data, test_user):
|
|
"""Test dashboard with 'year' filter."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project_with_data}/dashboard?period=year")
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestDashboardWithNoData:
|
|
"""Tests for dashboard behavior with minimal or no data."""
|
|
|
|
def test_dashboard_with_no_budget(self, app, client_fixture, test_client, test_user):
|
|
"""Test dashboard for project without budget."""
|
|
with app.app_context():
|
|
# Create project without budget
|
|
project = Project(name="No Budget Project", client_id=test_client, billable=False)
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{project.id}/dashboard")
|
|
assert response.status_code == 200
|
|
assert b"No budget set" in response.data
|
|
|
|
def test_dashboard_with_no_tasks(self, app, client_fixture, test_project, test_user):
|
|
"""Test dashboard for project without tasks."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project}/dashboard")
|
|
assert response.status_code == 200
|
|
assert b"No tasks" in response.data or b"0/0" in response.data
|
|
|
|
def test_dashboard_with_no_time_entries(self, app, client_fixture, test_project, test_user):
|
|
"""Test dashboard for project without time entries."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project}/dashboard")
|
|
assert response.status_code == 200
|
|
# Should show zero hours
|
|
project = db.session.get(Project, test_project)
|
|
assert project.total_hours == 0
|
|
|
|
def test_dashboard_with_no_activity(self, app, client_fixture, test_project, test_user):
|
|
"""Test dashboard for project without activity."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project}/dashboard")
|
|
assert response.status_code == 200
|
|
assert b"No recent activity" in response.data or b"Recent Activity" in response.data
|
|
|
|
|
|
class TestDashboardBudgetThreshold:
|
|
"""Tests for budget threshold warnings."""
|
|
|
|
def test_budget_threshold_exceeded_warning(self, app, client_fixture, test_client, test_user):
|
|
"""Test that budget threshold exceeded triggers warning."""
|
|
with app.app_context():
|
|
# Create project with budget
|
|
project = Project(
|
|
name="Budget Test Project",
|
|
client_id=test_client,
|
|
billable=True,
|
|
hourly_rate=Decimal("100.00"),
|
|
budget_amount=Decimal("500.00"), # Small budget
|
|
budget_threshold_percent=80,
|
|
)
|
|
project.estimated_hours = 10.0
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
|
|
# Add time entries to exceed threshold
|
|
now = datetime.now()
|
|
entry = TimeEntry(
|
|
user_id=test_user,
|
|
project_id=project.id,
|
|
start_time=now - timedelta(hours=6),
|
|
end_time=now,
|
|
duration_seconds=21600, # 6 hours = $600, exceeds $500 budget
|
|
billable=True,
|
|
)
|
|
db.session.add(entry)
|
|
db.session.commit()
|
|
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{project.id}/dashboard")
|
|
assert response.status_code == 200
|
|
|
|
# Check that budget warning appears
|
|
project = db.session.get(Project, project.id)
|
|
assert project.budget_threshold_exceeded
|
|
|
|
|
|
class TestDashboardNavigation:
|
|
"""Tests for dashboard navigation and links."""
|
|
|
|
def test_back_to_project_link(self, app, client_fixture, test_project, test_user):
|
|
"""Test that dashboard has link back to project view."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project}/dashboard")
|
|
assert response.status_code == 200
|
|
assert b"Back to Project" in response.data
|
|
assert f"/projects/{test_project}".encode() in response.data
|
|
|
|
def test_dashboard_link_in_project_view(self, app, client_fixture, test_project, test_user):
|
|
"""Test that project view has link to dashboard."""
|
|
with app.app_context():
|
|
login(client_fixture)
|
|
response = client_fixture.get(f"/projects/{test_project}")
|
|
assert response.status_code == 200
|
|
assert b"Dashboard" in response.data
|
|
assert f"/projects/{test_project}/dashboard".encode() in response.data
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|