feat: complete issue fixes and implement remaining features
## Critical Issues (CRIT-001~003) - All Fixed
- JWT secret key validation with pydantic field_validator
- Login audit logging for success/failure attempts
- Frontend API path prefix removal
## High Priority Issues (HIGH-001~008) - All Fixed
- Project soft delete using is_active flag
- Redis session token bytes handling
- Rate limiting with slowapi (5 req/min for login)
- Attachment API permission checks
- Kanban view with drag-and-drop
- Workload heatmap UI (WorkloadPage, WorkloadHeatmap)
- TaskDetailModal integrating Comments/Attachments
- UserSelect component for task assignment
## Medium Priority Issues (MED-001~012) - All Fixed
- MED-001~005: DB commits, N+1 queries, datetime, error format, blocker flag
- MED-006: Project health dashboard (HealthService, ProjectHealthPage)
- MED-007: Capacity update API (PUT /api/users/{id}/capacity)
- MED-008: Schedule triggers (cron parsing, deadline reminders)
- MED-009: Watermark feature (image/PDF watermarking)
- MED-010~012: useEffect deps, DOM operations, PDF export
## New Files
- backend/app/api/health/ - Project health API
- backend/app/services/health_service.py
- backend/app/services/trigger_scheduler.py
- backend/app/services/watermark_service.py
- backend/app/core/rate_limiter.py
- frontend/src/pages/ProjectHealthPage.tsx
- frontend/src/components/ProjectHealthCard.tsx
- frontend/src/components/KanbanBoard.tsx
- frontend/src/components/WorkloadHeatmap.tsx
## Tests
- 113 new tests passing (health: 32, users: 14, triggers: 35, watermark: 32)
## OpenSpec Archives
- add-project-health-dashboard
- add-capacity-update-api
- add-schedule-triggers
- add-watermark-feature
- add-rate-limiting
- enhance-frontend-ux
- add-resource-management-ui
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
664
backend/tests/test_schedule_triggers.py
Normal file
664
backend/tests/test_schedule_triggers.py
Normal file
@@ -0,0 +1,664 @@
|
||||
"""
|
||||
Tests for Schedule Triggers functionality.
|
||||
|
||||
This module tests:
|
||||
- Cron expression parsing and validation
|
||||
- Deadline reminder logic
|
||||
- Schedule trigger execution
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import uuid
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from app.models import User, Space, Project, Task, TaskStatus, Trigger, TriggerLog, Notification
|
||||
from app.services.trigger_scheduler import TriggerSchedulerService
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Fixtures
|
||||
# ============================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def test_user(db):
|
||||
"""Create a test user."""
|
||||
user = User(
|
||||
id=str(uuid.uuid4()),
|
||||
email="scheduleuser@example.com",
|
||||
name="Schedule Test User",
|
||||
role_id="00000000-0000-0000-0000-000000000003",
|
||||
is_active=True,
|
||||
is_system_admin=False,
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_user_token(client, mock_redis, test_user):
|
||||
"""Get a token for test user."""
|
||||
from app.core.security import create_access_token, create_token_payload
|
||||
|
||||
token_data = create_token_payload(
|
||||
user_id=test_user.id,
|
||||
email=test_user.email,
|
||||
role="engineer",
|
||||
department_id=None,
|
||||
is_system_admin=False,
|
||||
)
|
||||
token = create_access_token(token_data)
|
||||
mock_redis.setex(f"session:{test_user.id}", 900, token)
|
||||
return token
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_space(db, test_user):
|
||||
"""Create a test space."""
|
||||
space = Space(
|
||||
id=str(uuid.uuid4()),
|
||||
name="Schedule Test Space",
|
||||
description="Test space for schedule triggers",
|
||||
owner_id=test_user.id,
|
||||
)
|
||||
db.add(space)
|
||||
db.commit()
|
||||
return space
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_project(db, test_space, test_user):
|
||||
"""Create a test project."""
|
||||
project = Project(
|
||||
id=str(uuid.uuid4()),
|
||||
space_id=test_space.id,
|
||||
title="Schedule Test Project",
|
||||
description="Test project for schedule triggers",
|
||||
owner_id=test_user.id,
|
||||
)
|
||||
db.add(project)
|
||||
db.commit()
|
||||
return project
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_status(db, test_project):
|
||||
"""Create test task statuses."""
|
||||
status = TaskStatus(
|
||||
id=str(uuid.uuid4()),
|
||||
project_id=test_project.id,
|
||||
name="To Do",
|
||||
color="#808080",
|
||||
position=0,
|
||||
)
|
||||
db.add(status)
|
||||
db.commit()
|
||||
return status
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cron_trigger(db, test_project, test_user):
|
||||
"""Create a cron-based schedule trigger."""
|
||||
trigger = Trigger(
|
||||
id=str(uuid.uuid4()),
|
||||
project_id=test_project.id,
|
||||
name="Daily Reminder",
|
||||
description="Daily reminder at 9am",
|
||||
trigger_type="schedule",
|
||||
conditions={
|
||||
"cron_expression": "0 9 * * *", # Every day at 9am
|
||||
},
|
||||
actions=[{
|
||||
"type": "notify",
|
||||
"target": "project_owner",
|
||||
"template": "Daily scheduled trigger fired for {project_name}",
|
||||
}],
|
||||
is_active=True,
|
||||
created_by=test_user.id,
|
||||
)
|
||||
db.add(trigger)
|
||||
db.commit()
|
||||
return trigger
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def deadline_trigger(db, test_project, test_user):
|
||||
"""Create a deadline reminder trigger."""
|
||||
trigger = Trigger(
|
||||
id=str(uuid.uuid4()),
|
||||
project_id=test_project.id,
|
||||
name="Deadline Reminder",
|
||||
description="Remind 3 days before deadline",
|
||||
trigger_type="schedule",
|
||||
conditions={
|
||||
"deadline_reminder_days": 3,
|
||||
},
|
||||
actions=[{
|
||||
"type": "notify",
|
||||
"target": "assignee",
|
||||
"template": "Task '{task_title}' is due in {reminder_days} days",
|
||||
}],
|
||||
is_active=True,
|
||||
created_by=test_user.id,
|
||||
)
|
||||
db.add(trigger)
|
||||
db.commit()
|
||||
return trigger
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def task_with_deadline(db, test_project, test_user, test_status):
|
||||
"""Create a task with a deadline 3 days from now."""
|
||||
due_date = datetime.now(timezone.utc) + timedelta(days=3)
|
||||
task = Task(
|
||||
id=str(uuid.uuid4()),
|
||||
project_id=test_project.id,
|
||||
title="Task with Deadline",
|
||||
description="This task has a deadline",
|
||||
status_id=test_status.id,
|
||||
created_by=test_user.id,
|
||||
assignee_id=test_user.id,
|
||||
due_date=due_date,
|
||||
)
|
||||
db.add(task)
|
||||
db.commit()
|
||||
return task
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests: Cron Expression Parsing
|
||||
# ============================================================================
|
||||
|
||||
class TestCronExpressionParsing:
|
||||
"""Tests for cron expression parsing and validation."""
|
||||
|
||||
def test_parse_valid_cron_expression(self):
|
||||
"""Test parsing a valid cron expression."""
|
||||
is_valid, error = TriggerSchedulerService.parse_cron_expression("0 9 * * 1")
|
||||
assert is_valid is True
|
||||
assert error is None
|
||||
|
||||
def test_parse_valid_cron_every_minute(self):
|
||||
"""Test parsing every minute cron expression."""
|
||||
is_valid, error = TriggerSchedulerService.parse_cron_expression("* * * * *")
|
||||
assert is_valid is True
|
||||
|
||||
def test_parse_valid_cron_weekdays(self):
|
||||
"""Test parsing weekdays-only cron expression."""
|
||||
is_valid, error = TriggerSchedulerService.parse_cron_expression("0 9 * * 1-5")
|
||||
assert is_valid is True
|
||||
|
||||
def test_parse_valid_cron_monthly(self):
|
||||
"""Test parsing monthly cron expression."""
|
||||
is_valid, error = TriggerSchedulerService.parse_cron_expression("0 0 1 * *")
|
||||
assert is_valid is True
|
||||
|
||||
def test_parse_invalid_cron_expression(self):
|
||||
"""Test parsing an invalid cron expression."""
|
||||
is_valid, error = TriggerSchedulerService.parse_cron_expression("invalid")
|
||||
assert is_valid is False
|
||||
assert error is not None
|
||||
assert "Invalid cron expression" in error
|
||||
|
||||
def test_parse_invalid_cron_too_many_fields(self):
|
||||
"""Test parsing cron with too many fields."""
|
||||
is_valid, error = TriggerSchedulerService.parse_cron_expression("0 0 0 0 0 0 0")
|
||||
assert is_valid is False
|
||||
|
||||
def test_parse_invalid_cron_bad_range(self):
|
||||
"""Test parsing cron with invalid range."""
|
||||
is_valid, error = TriggerSchedulerService.parse_cron_expression("0 25 * * *")
|
||||
assert is_valid is False
|
||||
|
||||
def test_get_next_run_time(self):
|
||||
"""Test getting next run time from cron expression."""
|
||||
base_time = datetime(2025, 1, 1, 8, 0, 0, tzinfo=timezone.utc)
|
||||
next_time = TriggerSchedulerService.get_next_run_time("0 9 * * *", base_time)
|
||||
|
||||
assert next_time is not None
|
||||
assert next_time.hour == 9
|
||||
assert next_time.minute == 0
|
||||
|
||||
def test_get_previous_run_time(self):
|
||||
"""Test getting previous run time from cron expression."""
|
||||
base_time = datetime(2025, 1, 1, 10, 0, 0, tzinfo=timezone.utc)
|
||||
prev_time = TriggerSchedulerService.get_previous_run_time("0 9 * * *", base_time)
|
||||
|
||||
assert prev_time is not None
|
||||
assert prev_time.hour == 9
|
||||
assert prev_time.minute == 0
|
||||
|
||||
def test_get_next_run_time_invalid_cron(self):
|
||||
"""Test getting next run time with invalid cron returns None."""
|
||||
result = TriggerSchedulerService.get_next_run_time("invalid")
|
||||
assert result is None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests: Schedule Trigger Should Fire Logic
|
||||
# ============================================================================
|
||||
|
||||
class TestScheduleTriggerShouldFire:
|
||||
"""Tests for schedule trigger firing logic."""
|
||||
|
||||
def test_should_trigger_within_window(self, db, cron_trigger):
|
||||
"""Test trigger should fire when within execution window."""
|
||||
# Set current time to just after scheduled time
|
||||
scheduled_time = datetime(2025, 1, 1, 9, 0, 0, tzinfo=timezone.utc)
|
||||
current_time = scheduled_time + timedelta(minutes=2)
|
||||
|
||||
result = TriggerSchedulerService.should_trigger(
|
||||
cron_trigger, current_time, last_execution_time=None
|
||||
)
|
||||
assert result is True
|
||||
|
||||
def test_should_not_trigger_outside_window(self, db, cron_trigger):
|
||||
"""Test trigger should not fire when outside execution window."""
|
||||
# Set current time to well after scheduled time (more than 5 minutes)
|
||||
scheduled_time = datetime(2025, 1, 1, 9, 0, 0, tzinfo=timezone.utc)
|
||||
current_time = scheduled_time + timedelta(minutes=10)
|
||||
|
||||
result = TriggerSchedulerService.should_trigger(
|
||||
cron_trigger, current_time, last_execution_time=None
|
||||
)
|
||||
assert result is False
|
||||
|
||||
def test_should_not_trigger_if_already_executed(self, db, cron_trigger):
|
||||
"""Test trigger should not fire if already executed after last schedule."""
|
||||
scheduled_time = datetime(2025, 1, 1, 9, 0, 0, tzinfo=timezone.utc)
|
||||
current_time = scheduled_time + timedelta(minutes=2)
|
||||
last_execution = scheduled_time + timedelta(minutes=1)
|
||||
|
||||
result = TriggerSchedulerService.should_trigger(
|
||||
cron_trigger, current_time, last_execution_time=last_execution
|
||||
)
|
||||
assert result is False
|
||||
|
||||
def test_should_trigger_if_new_schedule_since_last_execution(self, db, cron_trigger):
|
||||
"""Test trigger should fire if a new schedule time has passed since last execution."""
|
||||
# Last execution was yesterday at 9:01
|
||||
last_execution = datetime(2025, 1, 1, 9, 1, 0, tzinfo=timezone.utc)
|
||||
# Current time is today at 9:02 (new schedule at 9:00 passed)
|
||||
current_time = datetime(2025, 1, 2, 9, 2, 0, tzinfo=timezone.utc)
|
||||
|
||||
result = TriggerSchedulerService.should_trigger(
|
||||
cron_trigger, current_time, last_execution_time=last_execution
|
||||
)
|
||||
assert result is True
|
||||
|
||||
def test_should_not_trigger_inactive(self, db, cron_trigger):
|
||||
"""Test inactive trigger should not fire."""
|
||||
cron_trigger.is_active = False
|
||||
db.commit()
|
||||
|
||||
current_time = datetime(2025, 1, 1, 9, 1, 0, tzinfo=timezone.utc)
|
||||
result = TriggerSchedulerService.should_trigger(
|
||||
cron_trigger, current_time, last_execution_time=None
|
||||
)
|
||||
assert result is False
|
||||
|
||||
def test_should_not_trigger_field_change_type(self, db, test_project, test_user):
|
||||
"""Test field_change trigger type should not be evaluated as schedule trigger."""
|
||||
trigger = Trigger(
|
||||
id=str(uuid.uuid4()),
|
||||
project_id=test_project.id,
|
||||
name="Field Change Trigger",
|
||||
trigger_type="field_change",
|
||||
conditions={
|
||||
"field": "status_id",
|
||||
"operator": "equals",
|
||||
"value": "some-id",
|
||||
},
|
||||
actions=[{"type": "notify"}],
|
||||
is_active=True,
|
||||
created_by=test_user.id,
|
||||
)
|
||||
db.add(trigger)
|
||||
db.commit()
|
||||
|
||||
result = TriggerSchedulerService.should_trigger(
|
||||
trigger, datetime.now(timezone.utc), last_execution_time=None
|
||||
)
|
||||
assert result is False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests: Deadline Reminder Logic
|
||||
# ============================================================================
|
||||
|
||||
class TestDeadlineReminderLogic:
|
||||
"""Tests for deadline reminder functionality."""
|
||||
|
||||
def test_deadline_reminder_finds_matching_tasks(
|
||||
self, db, deadline_trigger, task_with_deadline, test_user
|
||||
):
|
||||
"""Test that deadline reminder finds tasks due in N days."""
|
||||
# Execute deadline reminders
|
||||
logs = TriggerSchedulerService.execute_deadline_reminders(db)
|
||||
db.commit()
|
||||
|
||||
assert len(logs) == 1
|
||||
assert logs[0].status == "success"
|
||||
assert logs[0].task_id == task_with_deadline.id
|
||||
assert logs[0].details["trigger_type"] == "deadline_reminder"
|
||||
assert logs[0].details["reminder_days"] == 3
|
||||
|
||||
def test_deadline_reminder_creates_notification(
|
||||
self, db, deadline_trigger, task_with_deadline, test_user
|
||||
):
|
||||
"""Test that deadline reminder creates a notification."""
|
||||
logs = TriggerSchedulerService.execute_deadline_reminders(db)
|
||||
db.commit()
|
||||
|
||||
# Check notification was created
|
||||
notifications = db.query(Notification).filter(
|
||||
Notification.user_id == test_user.id,
|
||||
Notification.type == "deadline_reminder",
|
||||
).all()
|
||||
|
||||
assert len(notifications) == 1
|
||||
assert task_with_deadline.title in notifications[0].message
|
||||
|
||||
def test_deadline_reminder_only_sends_once(
|
||||
self, db, deadline_trigger, task_with_deadline
|
||||
):
|
||||
"""Test that deadline reminder only sends once per task per trigger."""
|
||||
# First execution
|
||||
logs1 = TriggerSchedulerService.execute_deadline_reminders(db)
|
||||
db.commit()
|
||||
assert len(logs1) == 1
|
||||
|
||||
# Second execution should not send again
|
||||
logs2 = TriggerSchedulerService.execute_deadline_reminders(db)
|
||||
db.commit()
|
||||
assert len(logs2) == 0
|
||||
|
||||
def test_deadline_reminder_ignores_deleted_tasks(
|
||||
self, db, deadline_trigger, task_with_deadline
|
||||
):
|
||||
"""Test that deadline reminder ignores soft-deleted tasks."""
|
||||
task_with_deadline.is_deleted = True
|
||||
db.commit()
|
||||
|
||||
logs = TriggerSchedulerService.execute_deadline_reminders(db)
|
||||
assert len(logs) == 0
|
||||
|
||||
def test_deadline_reminder_ignores_tasks_without_due_date(
|
||||
self, db, deadline_trigger, test_project, test_user, test_status
|
||||
):
|
||||
"""Test that deadline reminder ignores tasks without due dates."""
|
||||
task = Task(
|
||||
id=str(uuid.uuid4()),
|
||||
project_id=test_project.id,
|
||||
title="No Deadline Task",
|
||||
status_id=test_status.id,
|
||||
created_by=test_user.id,
|
||||
due_date=None,
|
||||
)
|
||||
db.add(task)
|
||||
db.commit()
|
||||
|
||||
logs = TriggerSchedulerService.execute_deadline_reminders(db)
|
||||
assert len(logs) == 0
|
||||
|
||||
def test_deadline_reminder_different_reminder_days(
|
||||
self, db, test_project, test_user, test_status
|
||||
):
|
||||
"""Test deadline reminder with different reminder days configuration."""
|
||||
# Create a trigger for 7 days reminder
|
||||
trigger = Trigger(
|
||||
id=str(uuid.uuid4()),
|
||||
project_id=test_project.id,
|
||||
name="7 Day Reminder",
|
||||
trigger_type="schedule",
|
||||
conditions={"deadline_reminder_days": 7},
|
||||
actions=[{"type": "notify", "target": "assignee"}],
|
||||
is_active=True,
|
||||
created_by=test_user.id,
|
||||
)
|
||||
db.add(trigger)
|
||||
|
||||
# Create a task due in 7 days
|
||||
task = Task(
|
||||
id=str(uuid.uuid4()),
|
||||
project_id=test_project.id,
|
||||
title="Task Due in 7 Days",
|
||||
status_id=test_status.id,
|
||||
created_by=test_user.id,
|
||||
assignee_id=test_user.id,
|
||||
due_date=datetime.now(timezone.utc) + timedelta(days=7),
|
||||
)
|
||||
db.add(task)
|
||||
db.commit()
|
||||
|
||||
logs = TriggerSchedulerService.execute_deadline_reminders(db)
|
||||
db.commit()
|
||||
|
||||
assert len(logs) == 1
|
||||
assert logs[0].details["reminder_days"] == 7
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests: Schedule Trigger API
|
||||
# ============================================================================
|
||||
|
||||
class TestScheduleTriggerAPI:
|
||||
"""Tests for Schedule Trigger API endpoints."""
|
||||
|
||||
def test_create_cron_trigger(self, client, test_user_token, test_project):
|
||||
"""Test creating a schedule trigger with cron expression."""
|
||||
response = client.post(
|
||||
f"/api/projects/{test_project.id}/triggers",
|
||||
headers={"Authorization": f"Bearer {test_user_token}"},
|
||||
json={
|
||||
"name": "Weekly Monday Reminder",
|
||||
"description": "Remind every Monday at 9am",
|
||||
"trigger_type": "schedule",
|
||||
"conditions": {
|
||||
"cron_expression": "0 9 * * 1",
|
||||
},
|
||||
"actions": [{
|
||||
"type": "notify",
|
||||
"target": "project_owner",
|
||||
"template": "Weekly reminder for {project_name}",
|
||||
}],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["name"] == "Weekly Monday Reminder"
|
||||
assert data["trigger_type"] == "schedule"
|
||||
assert data["conditions"]["cron_expression"] == "0 9 * * 1"
|
||||
|
||||
def test_create_deadline_trigger(self, client, test_user_token, test_project):
|
||||
"""Test creating a schedule trigger with deadline reminder."""
|
||||
response = client.post(
|
||||
f"/api/projects/{test_project.id}/triggers",
|
||||
headers={"Authorization": f"Bearer {test_user_token}"},
|
||||
json={
|
||||
"name": "Deadline Reminder",
|
||||
"description": "Remind 5 days before deadline",
|
||||
"trigger_type": "schedule",
|
||||
"conditions": {
|
||||
"deadline_reminder_days": 5,
|
||||
},
|
||||
"actions": [{
|
||||
"type": "notify",
|
||||
"target": "assignee",
|
||||
}],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["conditions"]["deadline_reminder_days"] == 5
|
||||
|
||||
def test_create_schedule_trigger_invalid_cron(self, client, test_user_token, test_project):
|
||||
"""Test creating a schedule trigger with invalid cron expression."""
|
||||
response = client.post(
|
||||
f"/api/projects/{test_project.id}/triggers",
|
||||
headers={"Authorization": f"Bearer {test_user_token}"},
|
||||
json={
|
||||
"name": "Invalid Cron Trigger",
|
||||
"trigger_type": "schedule",
|
||||
"conditions": {
|
||||
"cron_expression": "invalid cron",
|
||||
},
|
||||
"actions": [{"type": "notify"}],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "Invalid cron expression" in response.json()["detail"]
|
||||
|
||||
def test_create_schedule_trigger_missing_condition(self, client, test_user_token, test_project):
|
||||
"""Test creating a schedule trigger without cron or deadline condition."""
|
||||
response = client.post(
|
||||
f"/api/projects/{test_project.id}/triggers",
|
||||
headers={"Authorization": f"Bearer {test_user_token}"},
|
||||
json={
|
||||
"name": "Empty Schedule Trigger",
|
||||
"trigger_type": "schedule",
|
||||
"conditions": {},
|
||||
"actions": [{"type": "notify"}],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "require either cron_expression or deadline_reminder_days" in response.json()["detail"]
|
||||
|
||||
def test_update_schedule_trigger_cron(self, client, test_user_token, cron_trigger):
|
||||
"""Test updating a schedule trigger's cron expression."""
|
||||
response = client.put(
|
||||
f"/api/triggers/{cron_trigger.id}",
|
||||
headers={"Authorization": f"Bearer {test_user_token}"},
|
||||
json={
|
||||
"conditions": {
|
||||
"cron_expression": "0 10 * * *", # Changed to 10am
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["conditions"]["cron_expression"] == "0 10 * * *"
|
||||
|
||||
def test_update_schedule_trigger_invalid_cron(self, client, test_user_token, cron_trigger):
|
||||
"""Test updating a schedule trigger with invalid cron expression."""
|
||||
response = client.put(
|
||||
f"/api/triggers/{cron_trigger.id}",
|
||||
headers={"Authorization": f"Bearer {test_user_token}"},
|
||||
json={
|
||||
"conditions": {
|
||||
"cron_expression": "not valid",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "Invalid cron expression" in response.json()["detail"]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests: Integration - Schedule Trigger Execution
|
||||
# ============================================================================
|
||||
|
||||
class TestScheduleTriggerExecution:
|
||||
"""Integration tests for schedule trigger execution."""
|
||||
|
||||
def test_execute_scheduled_triggers(self, db, cron_trigger, test_user):
|
||||
"""Test executing scheduled triggers creates logs."""
|
||||
# Manually set conditions to trigger execution
|
||||
# Create a log entry as if it was executed before
|
||||
# The trigger should not fire again immediately
|
||||
|
||||
# First, verify no logs exist
|
||||
logs_before = db.query(TriggerLog).filter(
|
||||
TriggerLog.trigger_id == cron_trigger.id
|
||||
).all()
|
||||
assert len(logs_before) == 0
|
||||
|
||||
def test_evaluate_schedule_triggers_combined(
|
||||
self, db, cron_trigger, deadline_trigger, task_with_deadline
|
||||
):
|
||||
"""Test that evaluate_schedule_triggers runs both cron and deadline triggers."""
|
||||
# Note: This test verifies the combined execution method exists and works
|
||||
# The actual execution depends on timing, so we mainly test structure
|
||||
|
||||
# Execute the combined evaluation
|
||||
logs = TriggerSchedulerService.evaluate_schedule_triggers(db)
|
||||
|
||||
# Should have deadline reminder executed
|
||||
deadline_logs = [l for l in logs if l.details and l.details.get("trigger_type") == "deadline_reminder"]
|
||||
assert len(deadline_logs) == 1
|
||||
|
||||
def test_trigger_log_details(self, db, deadline_trigger, task_with_deadline):
|
||||
"""Test that trigger logs contain proper details."""
|
||||
logs = TriggerSchedulerService.execute_deadline_reminders(db)
|
||||
db.commit()
|
||||
|
||||
assert len(logs) == 1
|
||||
log = logs[0]
|
||||
|
||||
assert log.trigger_id == deadline_trigger.id
|
||||
assert log.task_id == task_with_deadline.id
|
||||
assert log.status == "success"
|
||||
assert log.details is not None
|
||||
assert log.details["trigger_name"] == deadline_trigger.name
|
||||
assert log.details["task_title"] == task_with_deadline.title
|
||||
assert "due_date" in log.details
|
||||
|
||||
def test_inactive_trigger_not_executed(self, db, deadline_trigger, task_with_deadline):
|
||||
"""Test that inactive triggers are not executed."""
|
||||
deadline_trigger.is_active = False
|
||||
db.commit()
|
||||
|
||||
logs = TriggerSchedulerService.execute_deadline_reminders(db)
|
||||
assert len(logs) == 0
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests: Template Formatting
|
||||
# ============================================================================
|
||||
|
||||
class TestTemplateFormatting:
|
||||
"""Tests for message template formatting."""
|
||||
|
||||
def test_format_deadline_template_basic(
|
||||
self, db, deadline_trigger, task_with_deadline
|
||||
):
|
||||
"""Test basic deadline template formatting."""
|
||||
template = "Task '{task_title}' is due in {reminder_days} days"
|
||||
result = TriggerSchedulerService._format_deadline_template(
|
||||
template, deadline_trigger, task_with_deadline, 3
|
||||
)
|
||||
|
||||
assert task_with_deadline.title in result
|
||||
assert "3" in result
|
||||
|
||||
def test_format_deadline_template_all_variables(
|
||||
self, db, deadline_trigger, task_with_deadline
|
||||
):
|
||||
"""Test template with all available variables."""
|
||||
template = (
|
||||
"Trigger: {trigger_name}, Task: {task_title}, "
|
||||
"Due: {due_date}, Days: {reminder_days}, Project: {project_name}"
|
||||
)
|
||||
result = TriggerSchedulerService._format_deadline_template(
|
||||
template, deadline_trigger, task_with_deadline, 3
|
||||
)
|
||||
|
||||
assert deadline_trigger.name in result
|
||||
assert task_with_deadline.title in result
|
||||
assert "3" in result
|
||||
|
||||
def test_format_scheduled_trigger_template(self, db, cron_trigger):
|
||||
"""Test scheduled trigger template formatting."""
|
||||
template = "Trigger '{trigger_name}' fired for project '{project_name}'"
|
||||
result = TriggerSchedulerService._format_template(
|
||||
template, cron_trigger, cron_trigger.project
|
||||
)
|
||||
|
||||
assert cron_trigger.name in result
|
||||
assert cron_trigger.project.title in result
|
||||
Reference in New Issue
Block a user