mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-06 11:40:52 -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.
460 lines
14 KiB
Python
460 lines
14 KiB
Python
"""Model tests for BudgetAlert"""
|
|
|
|
import pytest
|
|
from datetime import datetime, timedelta
|
|
from decimal import Decimal
|
|
from app import db
|
|
from app.models import BudgetAlert, Project, User, Client
|
|
|
|
|
|
@pytest.fixture
|
|
def client_obj(app):
|
|
"""Create a test client"""
|
|
client = Client(name="Test Client")
|
|
db.session.add(client)
|
|
db.session.commit()
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def project_with_budget(app, client_obj):
|
|
"""Create a test project with budget"""
|
|
project = Project(
|
|
name="Test Project",
|
|
client_id=client_obj.id,
|
|
billable=True,
|
|
hourly_rate=Decimal("100.00"),
|
|
budget_amount=Decimal("10000.00"),
|
|
budget_threshold_percent=80,
|
|
status="active",
|
|
)
|
|
db.session.add(project)
|
|
db.session.commit()
|
|
return project
|
|
|
|
|
|
@pytest.fixture
|
|
def test_user(app):
|
|
"""Create a test user"""
|
|
user = User(username="testuser", role="user")
|
|
user.is_active = True
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user
|
|
|
|
|
|
def test_budget_alert_creation(app, project_with_budget):
|
|
"""Test creating a budget alert"""
|
|
alert = BudgetAlert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_80",
|
|
alert_level="warning",
|
|
budget_consumed_percent=Decimal("82.5"),
|
|
budget_amount=Decimal("10000.00"),
|
|
consumed_amount=Decimal("8250.00"),
|
|
message="Warning: Project has consumed 82.5% of budget",
|
|
)
|
|
|
|
db.session.add(alert)
|
|
db.session.commit()
|
|
|
|
assert alert.id is not None
|
|
assert alert.project_id == project_with_budget.id
|
|
assert alert.alert_type == "warning_80"
|
|
assert alert.alert_level == "warning"
|
|
assert float(alert.budget_consumed_percent) == 82.5
|
|
assert not alert.is_acknowledged
|
|
assert alert.acknowledged_by is None
|
|
assert alert.acknowledged_at is None
|
|
|
|
|
|
def test_budget_alert_acknowledge(app, project_with_budget, test_user):
|
|
"""Test acknowledging a budget alert"""
|
|
alert = BudgetAlert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_80",
|
|
alert_level="warning",
|
|
budget_consumed_percent=Decimal("82.5"),
|
|
budget_amount=Decimal("10000.00"),
|
|
consumed_amount=Decimal("8250.00"),
|
|
message="Warning: Project has consumed 82.5% of budget",
|
|
)
|
|
|
|
db.session.add(alert)
|
|
db.session.commit()
|
|
|
|
# Acknowledge the alert
|
|
alert.acknowledge(test_user.id)
|
|
|
|
assert alert.is_acknowledged
|
|
assert alert.acknowledged_by == test_user.id
|
|
assert alert.acknowledged_at is not None
|
|
assert isinstance(alert.acknowledged_at, datetime)
|
|
|
|
|
|
def test_budget_alert_to_dict(app, project_with_budget):
|
|
"""Test converting budget alert to dictionary"""
|
|
alert = BudgetAlert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_80",
|
|
alert_level="warning",
|
|
budget_consumed_percent=Decimal("82.5"),
|
|
budget_amount=Decimal("10000.00"),
|
|
consumed_amount=Decimal("8250.00"),
|
|
message="Warning: Project has consumed 82.5% of budget",
|
|
)
|
|
|
|
db.session.add(alert)
|
|
db.session.commit()
|
|
|
|
alert_dict = alert.to_dict()
|
|
|
|
assert isinstance(alert_dict, dict)
|
|
assert alert_dict["id"] == alert.id
|
|
assert alert_dict["project_id"] == project_with_budget.id
|
|
assert alert_dict["project_name"] == project_with_budget.name
|
|
assert alert_dict["alert_type"] == "warning_80"
|
|
assert alert_dict["alert_level"] == "warning"
|
|
assert alert_dict["budget_consumed_percent"] == 82.5
|
|
assert alert_dict["budget_amount"] == 10000.0
|
|
assert alert_dict["consumed_amount"] == 8250.0
|
|
assert not alert_dict["is_acknowledged"]
|
|
assert alert_dict["acknowledged_by"] is None
|
|
assert alert_dict["acknowledged_at"] is None
|
|
|
|
|
|
def test_get_active_alerts(app, project_with_budget):
|
|
"""Test getting active alerts"""
|
|
# Create multiple alerts
|
|
alert1 = BudgetAlert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_80",
|
|
alert_level="warning",
|
|
budget_consumed_percent=Decimal("82.5"),
|
|
budget_amount=Decimal("10000.00"),
|
|
consumed_amount=Decimal("8250.00"),
|
|
message="Warning 1",
|
|
)
|
|
|
|
alert2 = BudgetAlert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_100",
|
|
alert_level="critical",
|
|
budget_consumed_percent=Decimal("100.0"),
|
|
budget_amount=Decimal("10000.00"),
|
|
consumed_amount=Decimal("10000.00"),
|
|
message="Warning 2",
|
|
)
|
|
|
|
db.session.add(alert1)
|
|
db.session.add(alert2)
|
|
db.session.commit()
|
|
|
|
# Get all active (unacknowledged) alerts
|
|
active_alerts = BudgetAlert.get_active_alerts()
|
|
|
|
assert len(active_alerts) == 2
|
|
assert all(not alert.is_acknowledged for alert in active_alerts)
|
|
|
|
|
|
def test_get_active_alerts_by_project(app, project_with_budget, client_obj):
|
|
"""Test getting active alerts for a specific project"""
|
|
# Create another project
|
|
project2 = Project(
|
|
name="Project 2",
|
|
client_id=client_obj.id,
|
|
billable=True,
|
|
hourly_rate=Decimal("100.00"),
|
|
budget_amount=Decimal("5000.00"),
|
|
status="active",
|
|
)
|
|
db.session.add(project2)
|
|
db.session.commit()
|
|
|
|
# Create alerts for both projects
|
|
alert1 = BudgetAlert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_80",
|
|
alert_level="warning",
|
|
budget_consumed_percent=Decimal("82.5"),
|
|
budget_amount=Decimal("10000.00"),
|
|
consumed_amount=Decimal("8250.00"),
|
|
message="Project 1 alert",
|
|
)
|
|
|
|
alert2 = BudgetAlert(
|
|
project_id=project2.id,
|
|
alert_type="warning_80",
|
|
alert_level="warning",
|
|
budget_consumed_percent=Decimal("85.0"),
|
|
budget_amount=Decimal("5000.00"),
|
|
consumed_amount=Decimal("4250.00"),
|
|
message="Project 2 alert",
|
|
)
|
|
|
|
db.session.add(alert1)
|
|
db.session.add(alert2)
|
|
db.session.commit()
|
|
|
|
# Get alerts for project 1 only
|
|
project1_alerts = BudgetAlert.get_active_alerts(project_id=project_with_budget.id)
|
|
|
|
assert len(project1_alerts) == 1
|
|
assert project1_alerts[0].project_id == project_with_budget.id
|
|
|
|
|
|
def test_create_alert_method(app, project_with_budget):
|
|
"""Test the create_alert class method"""
|
|
alert = BudgetAlert.create_alert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_80",
|
|
budget_consumed_percent=82.5,
|
|
budget_amount=10000.0,
|
|
consumed_amount=8250.0,
|
|
)
|
|
|
|
assert alert is not None
|
|
assert alert.id is not None
|
|
assert alert.alert_type == "warning_80"
|
|
assert alert.alert_level == "warning"
|
|
assert float(alert.budget_consumed_percent) == 82.5
|
|
assert "Warning: Project has consumed" in alert.message
|
|
|
|
|
|
def test_create_alert_critical_type(app, project_with_budget):
|
|
"""Test creating a critical alert"""
|
|
alert = BudgetAlert.create_alert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_100",
|
|
budget_consumed_percent=100.0,
|
|
budget_amount=10000.0,
|
|
consumed_amount=10000.0,
|
|
)
|
|
|
|
assert alert is not None
|
|
assert alert.alert_level == "critical"
|
|
|
|
|
|
def test_create_alert_over_budget(app, project_with_budget):
|
|
"""Test creating an over budget alert"""
|
|
alert = BudgetAlert.create_alert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="over_budget",
|
|
budget_consumed_percent=110.0,
|
|
budget_amount=10000.0,
|
|
consumed_amount=11000.0,
|
|
)
|
|
|
|
assert alert is not None
|
|
assert alert.alert_level == "critical"
|
|
assert "over budget" in alert.message.lower()
|
|
|
|
|
|
def test_create_alert_no_duplicates(app, project_with_budget):
|
|
"""Test that duplicate alerts are not created"""
|
|
# Create first alert
|
|
alert1 = BudgetAlert.create_alert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_80",
|
|
budget_consumed_percent=82.5,
|
|
budget_amount=10000.0,
|
|
consumed_amount=8250.0,
|
|
)
|
|
|
|
# Try to create duplicate alert
|
|
alert2 = BudgetAlert.create_alert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_80",
|
|
budget_consumed_percent=83.0,
|
|
budget_amount=10000.0,
|
|
consumed_amount=8300.0,
|
|
)
|
|
|
|
# Should return the existing alert, not create a new one
|
|
assert alert1.id == alert2.id
|
|
|
|
# Verify only one alert exists
|
|
all_alerts = BudgetAlert.query.filter_by(project_id=project_with_budget.id).all()
|
|
assert len(all_alerts) == 1
|
|
|
|
|
|
def test_get_alert_summary(app, project_with_budget, client_obj):
|
|
"""Test getting alert summary statistics"""
|
|
# Create multiple alerts with different statuses
|
|
alert1 = BudgetAlert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_80",
|
|
alert_level="warning",
|
|
budget_consumed_percent=Decimal("82.5"),
|
|
budget_amount=Decimal("10000.00"),
|
|
consumed_amount=Decimal("8250.00"),
|
|
message="Warning alert",
|
|
)
|
|
|
|
alert2 = BudgetAlert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_100",
|
|
alert_level="critical",
|
|
budget_consumed_percent=Decimal("100.0"),
|
|
budget_amount=Decimal("10000.00"),
|
|
consumed_amount=Decimal("10000.00"),
|
|
message="Critical alert",
|
|
)
|
|
|
|
# Create acknowledged alert
|
|
alert3 = BudgetAlert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_80",
|
|
alert_level="warning",
|
|
budget_consumed_percent=Decimal("85.0"),
|
|
budget_amount=Decimal("10000.00"),
|
|
consumed_amount=Decimal("8500.00"),
|
|
message="Acknowledged alert",
|
|
)
|
|
alert3.is_acknowledged = True
|
|
|
|
db.session.add(alert1)
|
|
db.session.add(alert2)
|
|
db.session.add(alert3)
|
|
db.session.commit()
|
|
|
|
summary = BudgetAlert.get_alert_summary()
|
|
|
|
assert summary["total_alerts"] == 3
|
|
assert summary["unacknowledged_alerts"] == 2
|
|
assert summary["critical_alerts"] == 1
|
|
|
|
|
|
def test_get_alert_summary_by_project(app, project_with_budget, client_obj):
|
|
"""Test getting alert summary for a specific project"""
|
|
# Create another project
|
|
project2 = Project(
|
|
name="Project 2",
|
|
client_id=client_obj.id,
|
|
billable=True,
|
|
hourly_rate=Decimal("100.00"),
|
|
budget_amount=Decimal("5000.00"),
|
|
status="active",
|
|
)
|
|
db.session.add(project2)
|
|
db.session.commit()
|
|
|
|
# Create alerts for both projects
|
|
alert1 = BudgetAlert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_80",
|
|
alert_level="warning",
|
|
budget_consumed_percent=Decimal("82.5"),
|
|
budget_amount=Decimal("10000.00"),
|
|
consumed_amount=Decimal("8250.00"),
|
|
message="Project 1 alert",
|
|
)
|
|
|
|
alert2 = BudgetAlert(
|
|
project_id=project2.id,
|
|
alert_type="warning_80",
|
|
alert_level="warning",
|
|
budget_consumed_percent=Decimal("85.0"),
|
|
budget_amount=Decimal("5000.00"),
|
|
consumed_amount=Decimal("4250.00"),
|
|
message="Project 2 alert",
|
|
)
|
|
|
|
db.session.add(alert1)
|
|
db.session.add(alert2)
|
|
db.session.commit()
|
|
|
|
# Get summary for project 1 only
|
|
summary = BudgetAlert.get_alert_summary(project_id=project_with_budget.id)
|
|
|
|
assert summary["total_alerts"] == 1
|
|
assert summary["unacknowledged_alerts"] == 1
|
|
|
|
|
|
def test_alert_repr(app, project_with_budget):
|
|
"""Test the string representation of a budget alert"""
|
|
alert = BudgetAlert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_80",
|
|
alert_level="warning",
|
|
budget_consumed_percent=Decimal("82.5"),
|
|
budget_amount=Decimal("10000.00"),
|
|
consumed_amount=Decimal("8250.00"),
|
|
message="Test alert",
|
|
)
|
|
|
|
db.session.add(alert)
|
|
db.session.commit()
|
|
|
|
repr_str = repr(alert)
|
|
assert "BudgetAlert" in repr_str
|
|
assert "warning_80" in repr_str
|
|
assert str(project_with_budget.id) in repr_str
|
|
|
|
|
|
def test_alert_message_generation(app, project_with_budget):
|
|
"""Test alert message generation for different types"""
|
|
# Test warning_80 message
|
|
alert1 = BudgetAlert.create_alert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_80",
|
|
budget_consumed_percent=82.5,
|
|
budget_amount=10000.0,
|
|
consumed_amount=8250.0,
|
|
)
|
|
assert "Warning" in alert1.message
|
|
assert "82.5%" in alert1.message or "82.5" in alert1.message
|
|
|
|
# Test warning_100 message
|
|
alert2 = BudgetAlert.create_alert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_100",
|
|
budget_consumed_percent=100.0,
|
|
budget_amount=10000.0,
|
|
consumed_amount=10000.0,
|
|
)
|
|
assert "reached 100%" in alert2.message.lower() or "alert" in alert2.message.lower()
|
|
|
|
# Test over_budget message
|
|
alert3 = BudgetAlert.create_alert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="over_budget",
|
|
budget_consumed_percent=110.0,
|
|
budget_amount=10000.0,
|
|
consumed_amount=11000.0,
|
|
)
|
|
assert "over budget" in alert3.message.lower() or "critical" in alert3.message.lower()
|
|
|
|
|
|
def test_acknowledged_alerts_filter(app, project_with_budget, test_user):
|
|
"""Test filtering for acknowledged alerts"""
|
|
# Create alerts
|
|
alert1 = BudgetAlert.create_alert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_80",
|
|
budget_consumed_percent=82.5,
|
|
budget_amount=10000.0,
|
|
consumed_amount=8250.0,
|
|
)
|
|
|
|
alert2 = BudgetAlert.create_alert(
|
|
project_id=project_with_budget.id,
|
|
alert_type="warning_100",
|
|
budget_consumed_percent=100.0,
|
|
budget_amount=10000.0,
|
|
consumed_amount=10000.0,
|
|
)
|
|
|
|
# Acknowledge one alert
|
|
alert1.acknowledge(test_user.id)
|
|
|
|
# Get unacknowledged alerts
|
|
unacknowledged = BudgetAlert.get_active_alerts(acknowledged=False)
|
|
assert len(unacknowledged) == 1
|
|
assert unacknowledged[0].id == alert2.id
|
|
|
|
# Get acknowledged alerts
|
|
acknowledged = BudgetAlert.get_active_alerts(acknowledged=True)
|
|
assert len(acknowledged) == 1
|
|
assert acknowledged[0].id == alert1.id
|