Fix test failures and workload/websocket behavior

This commit is contained in:
beabigegg
2026-01-11 08:37:21 +08:00
parent 3bdc6ff1c9
commit f5f870da56
49 changed files with 3006 additions and 1132 deletions

View File

@@ -24,6 +24,11 @@ engine = create_engine(
)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Ensure app code paths that use SessionLocal directly hit the test DB
from app.core import database as database_module
database_module.engine = engine
database_module.SessionLocal = TestingSessionLocal
class MockRedis:
"""Mock Redis client for testing."""
@@ -102,7 +107,11 @@ def db():
@pytest.fixture(scope="function")
def mock_redis():
"""Create mock Redis for testing."""
return MockRedis()
from app.core import redis as redis_module
client = redis_module.redis_client
if hasattr(client, "store"):
client.store.clear()
return client
@pytest.fixture(scope="function")

View File

@@ -10,6 +10,7 @@ from app.api.dashboard.router import (
get_workload_summary,
get_health_summary,
)
from app.services.workload_service import get_week_bounds
from app.schemas.workload import LoadLevel
@@ -99,15 +100,16 @@ class TestTaskStatistics:
):
"""Helper to create a task with optional characteristics."""
now = datetime.utcnow()
week_start, week_end = get_week_bounds(now.date())
if overdue:
due_date = now - timedelta(days=3)
due_date = datetime.combine(week_start, datetime.min.time()) - timedelta(days=1)
elif due_this_week:
# Due in the middle of current week
due_date = now + timedelta(days=2)
due_date = datetime.combine(week_start, datetime.min.time()) + timedelta(days=2)
else:
# Due next week
due_date = now + timedelta(days=10)
due_date = datetime.combine(week_end, datetime.min.time()) + timedelta(days=2)
task = Task(
id=task_id,
@@ -313,13 +315,26 @@ class TestWorkloadSummary:
assert workload.load_percentage == Decimal("0.00")
assert workload.load_level == LoadLevel.NORMAL
def test_zero_capacity(self, db):
"""User with zero capacity should show unavailable load level."""
data = self.setup_test_data(db)
data["user"].capacity = 0
db.commit()
workload = get_workload_summary(db, data["user"])
assert workload.capacity_hours == Decimal("0")
assert workload.load_percentage is None
assert workload.load_level == LoadLevel.UNAVAILABLE
def test_workload_with_tasks(self, db):
"""Should calculate correct allocated hours."""
data = self.setup_test_data(db)
# Create tasks due this week with estimates
now = datetime.utcnow()
due_date = now + timedelta(days=2)
week_start, _ = get_week_bounds(now.date())
due_date = datetime.combine(week_start, datetime.min.time()) + timedelta(days=2)
task1 = Task(
id="task-wl-1",
@@ -359,7 +374,8 @@ class TestWorkloadSummary:
data = self.setup_test_data(db)
now = datetime.utcnow()
due_date = now + timedelta(days=2)
week_start, _ = get_week_bounds(now.date())
due_date = datetime.combine(week_start, datetime.min.time()) + timedelta(days=2)
# Create task with 48h estimate (> 40h capacity)
task = Task(
@@ -508,6 +524,7 @@ class TestDashboardAPI:
# Create a task for the admin user
now = datetime.utcnow()
week_start, _ = get_week_bounds(now.date())
task = Task(
id="task-api-dash-001",
project_id="project-api-dash-001",
@@ -515,7 +532,7 @@ class TestDashboardAPI:
assignee_id="00000000-0000-0000-0000-000000000001",
status_id="status-api-dash-todo",
original_estimate=Decimal("8"),
due_date=now + timedelta(days=2),
due_date=datetime.combine(week_start, datetime.min.time()) + timedelta(days=2),
created_by="00000000-0000-0000-0000-000000000001",
is_deleted=False,
)

View File

@@ -1,7 +1,7 @@
import pytest
import uuid
from datetime import datetime, timedelta
from app.models import User, Space, Project, Task, TaskStatus, ScheduledReport, ReportHistory, Blocker
from app.models import User, Space, Project, Task, TaskStatus, ScheduledReport, ReportHistory, Blocker, ProjectMember
from app.services.report_service import ReportService
@@ -76,6 +76,7 @@ def test_statuses(db, test_project):
name="To Do",
color="#808080",
position=0,
is_done=False,
)
in_progress = TaskStatus(
id=str(uuid.uuid4()),
@@ -83,6 +84,7 @@ def test_statuses(db, test_project):
name="In Progress",
color="#0000FF",
position=1,
is_done=False,
)
done = TaskStatus(
id=str(uuid.uuid4()),
@@ -90,6 +92,7 @@ def test_statuses(db, test_project):
name="Done",
color="#00FF00",
position=2,
is_done=True,
)
db.add_all([todo, in_progress, done])
db.commit()
@@ -165,12 +168,90 @@ class TestReportService:
stats = ReportService.get_weekly_stats(db, test_user.id)
assert stats["summary"]["completed_count"] == 1
assert stats["summary"]["in_progress_count"] == 1
assert stats["summary"]["in_progress_count"] == 2
assert stats["summary"]["overdue_count"] == 1
assert stats["summary"]["total_tasks"] == 3
assert len(stats["projects"]) == 1
assert stats["projects"][0]["project_title"] == "Report Test Project"
def test_weekly_stats_includes_project_members(self, db, test_user, test_space):
"""Project member should receive weekly stats for member projects."""
other_owner = User(
id=str(uuid.uuid4()),
email="owner2@example.com",
name="Other Owner",
role_id="00000000-0000-0000-0000-000000000003",
is_active=True,
is_system_admin=False,
)
db.add(other_owner)
db.commit()
member_project = Project(
id=str(uuid.uuid4()),
space_id=test_space.id,
title="Member Project",
description="Project for member stats",
owner_id=other_owner.id,
)
db.add(member_project)
db.commit()
db.add(ProjectMember(
id=str(uuid.uuid4()),
project_id=member_project.id,
user_id=test_user.id,
role="member",
added_by=other_owner.id,
))
db.commit()
member_status = TaskStatus(
id=str(uuid.uuid4()),
project_id=member_project.id,
name="In Progress",
color="#0000FF",
position=0,
is_done=False,
)
db.add(member_status)
db.commit()
task = Task(
id=str(uuid.uuid4()),
project_id=member_project.id,
title="Member Task",
status_id=member_status.id,
created_by=other_owner.id,
)
db.add(task)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
project_titles = {project["project_title"] for project in stats["projects"]}
assert "Member Project" in project_titles
def test_completed_task_outside_week_not_counted(self, db, test_user, test_project, test_statuses):
"""Completed tasks outside the week window should not be counted."""
week_start = ReportService.get_week_start()
week_end = week_start + timedelta(days=7)
task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="Completed Outside Week",
status_id=test_statuses["done"].id,
created_by=test_user.id,
)
task.updated_at = week_end + timedelta(days=1)
db.add(task)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id, week_start)
assert stats["summary"]["completed_count"] == 0
def test_generate_weekly_report(self, db, test_user, test_project, test_tasks, test_statuses):
"""Test generating a weekly report."""
report = ReportService.generate_weekly_report(db, test_user.id)
@@ -216,6 +297,45 @@ class TestReportAPI:
assert "report_id" in data
assert "summary" in data
def test_weekly_report_subscription_toggle(self, client, test_user_token, db, test_user):
"""Test weekly report subscription toggle endpoints."""
response = client.get(
"/api/reports/weekly/subscription",
headers={"Authorization": f"Bearer {test_user_token}"},
)
assert response.status_code == 200
assert response.json()["is_active"] is False
response = client.put(
"/api/reports/weekly/subscription",
headers={"Authorization": f"Bearer {test_user_token}"},
json={"is_active": True},
)
assert response.status_code == 200
assert response.json()["is_active"] is True
response = client.get(
"/api/reports/weekly/subscription",
headers={"Authorization": f"Bearer {test_user_token}"},
)
assert response.status_code == 200
assert response.json()["is_active"] is True
response = client.put(
"/api/reports/weekly/subscription",
headers={"Authorization": f"Bearer {test_user_token}"},
json={"is_active": False},
)
assert response.status_code == 200
assert response.json()["is_active"] is False
scheduled = db.query(ScheduledReport).filter(
ScheduledReport.recipient_id == test_user.id,
ScheduledReport.report_type == "weekly",
).first()
assert scheduled is not None
assert scheduled.is_active is False
def test_list_report_history_empty(self, client, test_user_token):
"""Test listing report history when empty."""
response = client.get(

View File

@@ -1,7 +1,13 @@
import pytest
import uuid
from app.models import User, Space, Project, Task, TaskStatus, Trigger, TriggerLog, Notification
from datetime import datetime
from app.models import (
User, Space, Project, Task, TaskStatus, Trigger, TriggerLog, Notification,
CustomField, ProjectMember, Department, Role
)
from app.services.trigger_service import TriggerService
from app.services.custom_value_service import CustomValueService
from app.schemas.task import CustomValueInput
@pytest.fixture
@@ -188,6 +194,39 @@ class TestTriggerService:
result = TriggerService._check_conditions(conditions, old_values, new_values)
assert result is True
def test_check_conditions_composite_and(self, db, test_status):
"""Test composite AND conditions with one unchanged rule."""
conditions = {
"logic": "and",
"rules": [
{"field": "status_id", "operator": "changed_to", "value": test_status[1].id},
{"field": "priority", "operator": "equals", "value": "high"},
],
}
old_values = {"status_id": test_status[0].id, "priority": "high"}
new_values = {"status_id": test_status[1].id, "priority": "high"}
result = TriggerService._check_conditions(conditions, old_values, new_values)
assert result is True
def test_check_conditions_due_date_in_range_inclusive(self, db):
"""Test due_date in range operator is inclusive."""
conditions = {
"logic": "and",
"rules": [
{
"field": "due_date",
"operator": "in",
"value": {"start": "2024-01-01", "end": "2024-01-15"},
}
],
}
old_values = {"due_date": datetime(2024, 1, 10)}
new_values = {"due_date": datetime(2024, 1, 15)}
result = TriggerService._check_conditions(conditions, old_values, new_values)
assert result is True
def test_evaluate_triggers_creates_notification(self, db, test_task, test_trigger, test_user, test_status):
"""Test that evaluate_triggers creates notification when conditions match."""
# Create another user to receive notification
@@ -229,6 +268,247 @@ class TestTriggerService:
assert len(logs) == 0
def test_custom_field_formula_condition(self, db, test_task, test_project, test_user):
"""Test formula custom field conditions are evaluated."""
number_field = CustomField(
id=str(uuid.uuid4()),
project_id=test_project.id,
name="Points",
field_type="number",
position=0,
)
formula_field = CustomField(
id=str(uuid.uuid4()),
project_id=test_project.id,
name="Double Points",
field_type="formula",
formula="{Points} * 2",
position=1,
)
db.add_all([number_field, formula_field])
db.commit()
CustomValueService.save_custom_values(
db,
test_task,
[CustomValueInput(field_id=number_field.id, value=3)],
)
db.commit()
old_custom_values = {
cv.field_id: cv.value
for cv in CustomValueService.get_custom_values_for_task(db, test_task)
}
CustomValueService.save_custom_values(
db,
test_task,
[CustomValueInput(field_id=number_field.id, value=4)],
)
db.commit()
new_custom_values = {
cv.field_id: cv.value
for cv in CustomValueService.get_custom_values_for_task(db, test_task)
}
trigger = Trigger(
id=str(uuid.uuid4()),
project_id=test_project.id,
name="Formula Trigger",
description="Notify when formula changes to 8",
trigger_type="field_change",
conditions={
"field": "custom_fields",
"field_id": formula_field.id,
"operator": "changed_to",
"value": "8",
},
actions=[{"type": "notify", "target": f"user:{test_user.id}"}],
is_active=True,
created_by=test_user.id,
)
db.add(trigger)
db.commit()
logs = TriggerService.evaluate_triggers(
db,
test_task,
{"custom_fields": old_custom_values},
{"custom_fields": new_custom_values},
test_user,
)
db.commit()
assert len(logs) == 1
assert logs[0].status == "success"
class TestTriggerNotifications:
"""Tests for trigger notification target resolution."""
def test_notify_project_members_excludes_triggerer(self, db, test_task, test_project, test_user, test_status):
member_user = User(
id=str(uuid.uuid4()),
email="member@example.com",
name="Member User",
role_id="00000000-0000-0000-0000-000000000003",
is_active=True,
)
other_member = User(
id=str(uuid.uuid4()),
email="member2@example.com",
name="Other Member",
role_id="00000000-0000-0000-0000-000000000003",
is_active=True,
)
db.add_all([member_user, other_member])
db.commit()
db.add_all([
ProjectMember(
id=str(uuid.uuid4()),
project_id=test_project.id,
user_id=member_user.id,
role="member",
added_by=test_user.id,
),
ProjectMember(
id=str(uuid.uuid4()),
project_id=test_project.id,
user_id=other_member.id,
role="member",
added_by=test_user.id,
),
ProjectMember(
id=str(uuid.uuid4()),
project_id=test_project.id,
user_id=test_user.id,
role="member",
added_by=test_user.id,
),
])
db.commit()
trigger = Trigger(
id=str(uuid.uuid4()),
project_id=test_project.id,
name="Project Members Trigger",
description="Notify all project members",
trigger_type="field_change",
conditions={
"field": "status_id",
"operator": "changed_to",
"value": test_status[1].id,
},
actions=[{"type": "notify", "target": "project_members"}],
is_active=True,
created_by=test_user.id,
)
db.add(trigger)
db.commit()
logs = TriggerService.evaluate_triggers(
db,
test_task,
{"status_id": test_status[0].id},
{"status_id": test_status[1].id},
member_user,
)
db.commit()
assert len(logs) == 1
assert db.query(Notification).filter(Notification.user_id == member_user.id).count() == 0
assert db.query(Notification).filter(Notification.user_id == other_member.id).count() == 1
assert db.query(Notification).filter(Notification.user_id == test_user.id).count() == 1
def test_notify_department_and_role_targets(self, db, test_task, test_project, test_user, test_status):
department = Department(
id=str(uuid.uuid4()),
name="QA Department",
)
qa_role = Role(
id=str(uuid.uuid4()),
name="qa",
permissions={},
is_system_role=False,
)
db.add_all([department, qa_role])
db.commit()
triggerer = User(
id=str(uuid.uuid4()),
email="qa_lead@example.com",
name="QA Lead",
role_id=qa_role.id,
department_id=department.id,
is_active=True,
)
dept_user = User(
id=str(uuid.uuid4()),
email="dept_user@example.com",
name="Dept User",
role_id="00000000-0000-0000-0000-000000000003",
department_id=department.id,
is_active=True,
)
role_user = User(
id=str(uuid.uuid4()),
email="role_user@example.com",
name="Role User",
role_id=qa_role.id,
department_id=None,
is_active=True,
)
db.add_all([triggerer, dept_user, role_user])
db.commit()
dept_trigger = Trigger(
id=str(uuid.uuid4()),
project_id=test_project.id,
name="Department Trigger",
description="Notify department",
trigger_type="field_change",
conditions={
"field": "status_id",
"operator": "changed_to",
"value": test_status[1].id,
},
actions=[{"type": "notify", "target": f"department:{department.id}"}],
is_active=True,
created_by=test_user.id,
)
role_trigger = Trigger(
id=str(uuid.uuid4()),
project_id=test_project.id,
name="Role Trigger",
description="Notify role",
trigger_type="field_change",
conditions={
"field": "status_id",
"operator": "changed_to",
"value": test_status[1].id,
},
actions=[{"type": "notify", "target": f"role:{qa_role.name}"}],
is_active=True,
created_by=test_user.id,
)
db.add_all([dept_trigger, role_trigger])
db.commit()
TriggerService.evaluate_triggers(
db,
test_task,
{"status_id": test_status[0].id},
{"status_id": test_status[1].id},
triggerer,
)
db.commit()
assert db.query(Notification).filter(Notification.user_id == triggerer.id).count() == 0
assert db.query(Notification).filter(Notification.user_id == dept_user.id).count() == 1
assert db.query(Notification).filter(Notification.user_id == role_user.id).count() == 1
class TestTriggerAPI:
"""Tests for Trigger API endpoints."""

View File

@@ -195,6 +195,19 @@ class TestWorkloadService:
assert summary.load_level == LoadLevel.NORMAL
assert summary.task_count == 0
def test_calculate_user_workload_zero_capacity(self, db):
"""User with zero capacity should return unavailable load level."""
data = self.setup_test_data(db)
data["engineer"].capacity = 0
db.commit()
week_start = date(2024, 1, 1)
summary = calculate_user_workload(db, data["engineer"], week_start)
assert summary.capacity_hours == Decimal("0")
assert summary.load_percentage is None
assert summary.load_level == LoadLevel.UNAVAILABLE
def test_calculate_user_workload_with_tasks(self, db):
"""User with tasks should have correct allocated hours."""
data = self.setup_test_data(db)
@@ -445,6 +458,7 @@ class TestWorkloadAccessControl:
def setup_test_data(self, db, mock_redis):
"""Set up test data with two departments."""
from app.core.security import create_access_token, create_token_payload
from app.services.workload_service import get_current_week_start
# Create departments
dept_rd = Department(id="dept-rd", name="R&D")
@@ -478,6 +492,38 @@ class TestWorkloadAccessControl:
)
db.add(engineer_ops)
# Create space and project for workload task
space = Space(
id="space-wl-acl-001",
name="Workload ACL Space",
owner_id="00000000-0000-0000-0000-000000000001",
is_active=True,
)
db.add(space)
project = Project(
id="project-wl-acl-001",
space_id=space.id,
title="Workload ACL Project",
owner_id="00000000-0000-0000-0000-000000000001",
department_id=dept_rd.id,
security_level="department",
)
db.add(project)
# Create a task for the R&D engineer so they appear in heatmap
week_start = get_current_week_start()
due_date = datetime.combine(week_start, datetime.min.time()) + timedelta(days=2)
task = Task(
id="task-wl-acl-001",
project_id=project.id,
title="Workload ACL Task",
assignee_id=engineer_rd.id,
due_date=due_date,
created_by="00000000-0000-0000-0000-000000000001",
)
db.add(task)
db.commit()
# Create token for R&D engineer
@@ -514,6 +560,18 @@ class TestWorkloadAccessControl:
assert len(result["users"]) == 1
assert result["users"][0]["user_id"] == "user-rd-001"
def test_regular_user_cannot_filter_other_user_ids(self, client, db, mock_redis):
"""Regular user should not filter workload for other users."""
data = self.setup_test_data(db, mock_redis)
user_ids = f"{data['engineer_rd'].id},{data['engineer_ops'].id}"
response = client.get(
f"/api/workload/heatmap?user_ids={user_ids}",
headers={"Authorization": f"Bearer {data['rd_token']}"},
)
assert response.status_code == 403
def test_regular_user_cannot_access_other_department(self, client, db, mock_redis):
"""Regular user should not access other department's workload."""
data = self.setup_test_data(db, mock_redis)