1+ """
2+ Tests for trigger_cron functions to improve code coverage.
3+ These are pure unit tests that mock database operations.
4+ Environment variables are provided by CI (see .github/workflows/test-state-manager.yml).
5+ """
6+ import pytest
7+ from unittest .mock import MagicMock , AsyncMock , patch
8+ from datetime import datetime
9+
10+ from app .tasks .trigger_cron import create_next_triggers
11+ from app .models .trigger_models import TriggerStatusEnum , TriggerTypeEnum
12+
13+
14+ @pytest .mark .asyncio
15+ async def test_create_next_triggers_with_america_new_york_timezone ():
16+ """Test create_next_triggers processes America/New_York timezone correctly"""
17+ trigger = MagicMock ()
18+ trigger .expression = "0 9 * * *"
19+ trigger .timezone = "America/New_York"
20+ trigger .trigger_time = datetime (2025 , 10 , 4 , 13 , 0 , 0 ) # Naive UTC time
21+ trigger .graph_name = "test_graph"
22+ trigger .namespace = "test_namespace"
23+
24+ cron_time = datetime (2025 , 10 , 6 , 0 , 0 , 0 )
25+
26+ with patch ('app.tasks.trigger_cron.DatabaseTriggers' ) as mock_db_class :
27+ mock_instance = MagicMock ()
28+ mock_instance .insert = AsyncMock ()
29+ mock_db_class .return_value = mock_instance
30+
31+ await create_next_triggers (trigger , cron_time )
32+
33+ # Verify DatabaseTriggers was instantiated with timezone
34+ assert mock_db_class .called
35+ call_kwargs = mock_db_class .call_args [1 ]
36+ assert call_kwargs ['timezone' ] == "America/New_York"
37+ assert call_kwargs ['expression' ] == "0 9 * * *"
38+
39+
40+ @pytest .mark .asyncio
41+ async def test_create_next_triggers_with_utc_timezone ():
42+ """Test create_next_triggers with UTC timezone"""
43+ trigger = MagicMock ()
44+ trigger .expression = "0 9 * * *"
45+ trigger .timezone = "UTC"
46+ trigger .trigger_time = datetime (2025 , 10 , 4 , 9 , 0 , 0 )
47+ trigger .graph_name = "test_graph"
48+ trigger .namespace = "test_namespace"
49+
50+ cron_time = datetime (2025 , 10 , 6 , 0 , 0 , 0 )
51+
52+ with patch ('app.tasks.trigger_cron.DatabaseTriggers' ) as mock_db_class :
53+ mock_instance = MagicMock ()
54+ mock_instance .insert = AsyncMock ()
55+ mock_db_class .return_value = mock_instance
56+
57+ await create_next_triggers (trigger , cron_time )
58+
59+ # Verify timezone was passed correctly
60+ call_kwargs = mock_db_class .call_args [1 ]
61+ assert call_kwargs ['timezone' ] == "UTC"
62+
63+
64+ @pytest .mark .asyncio
65+ async def test_create_next_triggers_with_none_timezone_defaults_to_utc ():
66+ """Test create_next_triggers with None timezone defaults to UTC"""
67+ trigger = MagicMock ()
68+ trigger .expression = "0 9 * * *"
69+ trigger .timezone = None
70+ trigger .trigger_time = datetime (2025 , 10 , 4 , 9 , 0 , 0 )
71+ trigger .graph_name = "test_graph"
72+ trigger .namespace = "test_namespace"
73+
74+ cron_time = datetime (2025 , 10 , 6 , 0 , 0 , 0 )
75+
76+ with patch ('app.tasks.trigger_cron.DatabaseTriggers' ) as mock_db_class :
77+ mock_instance = MagicMock ()
78+ mock_instance .insert = AsyncMock ()
79+ mock_db_class .return_value = mock_instance
80+
81+ await create_next_triggers (trigger , cron_time )
82+
83+ # Verify None timezone is passed through (will default to UTC in ZoneInfo call)
84+ call_kwargs = mock_db_class .call_args [1 ]
85+ assert call_kwargs ['timezone' ] is None
86+
87+
88+ @pytest .mark .asyncio
89+ async def test_create_next_triggers_with_europe_london_timezone ():
90+ """Test create_next_triggers with Europe/London timezone"""
91+ trigger = MagicMock ()
92+ trigger .expression = "0 17 * * *"
93+ trigger .timezone = "Europe/London"
94+ trigger .trigger_time = datetime (2025 , 10 , 4 , 16 , 0 , 0 ) # UTC time
95+ trigger .graph_name = "test_graph"
96+ trigger .namespace = "test_namespace"
97+
98+ cron_time = datetime (2025 , 10 , 6 , 0 , 0 , 0 )
99+
100+ with patch ('app.tasks.trigger_cron.DatabaseTriggers' ) as mock_db_class :
101+ mock_instance = MagicMock ()
102+ mock_instance .insert = AsyncMock ()
103+ mock_db_class .return_value = mock_instance
104+
105+ await create_next_triggers (trigger , cron_time )
106+
107+ # Verify Europe/London timezone was used
108+ call_kwargs = mock_db_class .call_args [1 ]
109+ assert call_kwargs ['timezone' ] == "Europe/London"
110+
111+
112+ @pytest .mark .asyncio
113+ async def test_create_next_triggers_handles_duplicate_key_error ():
114+ """Test create_next_triggers handles DuplicateKeyError gracefully"""
115+ from pymongo .errors import DuplicateKeyError
116+
117+ trigger = MagicMock ()
118+ trigger .expression = "0 9 * * *"
119+ trigger .timezone = "America/New_York"
120+ trigger .trigger_time = datetime (2025 , 10 , 4 , 13 , 0 , 0 )
121+ trigger .graph_name = "test_graph"
122+ trigger .namespace = "test_namespace"
123+
124+ cron_time = datetime (2025 , 10 , 6 , 0 , 0 , 0 )
125+
126+ with patch ('app.tasks.trigger_cron.DatabaseTriggers' ) as mock_db_class :
127+ mock_instance = MagicMock ()
128+ # First call raises DuplicateKeyError, second succeeds
129+ mock_instance .insert = AsyncMock (side_effect = [
130+ DuplicateKeyError ("Duplicate" ),
131+ None
132+ ])
133+ mock_db_class .return_value = mock_instance
134+
135+ with patch ('app.tasks.trigger_cron.logger' ) as mock_logger :
136+ # Should not raise exception
137+ await create_next_triggers (trigger , cron_time )
138+
139+ # Verify error was logged
140+ assert mock_logger .error .called
141+ error_msg = mock_logger .error .call_args [0 ][0 ]
142+ assert "Duplicate trigger found" in error_msg
143+
144+
145+ @pytest .mark .asyncio
146+ async def test_create_next_triggers_trigger_time_is_datetime ():
147+ """Test that next trigger_time is a datetime object"""
148+ trigger = MagicMock ()
149+ trigger .expression = "0 9 * * *"
150+ trigger .timezone = "America/New_York"
151+ trigger .trigger_time = datetime (2025 , 10 , 4 , 13 , 0 , 0 )
152+ trigger .graph_name = "test_graph"
153+ trigger .namespace = "test_namespace"
154+
155+ cron_time = datetime (2025 , 10 , 6 , 0 , 0 , 0 )
156+
157+ with patch ('app.tasks.trigger_cron.DatabaseTriggers' ) as mock_db_class :
158+ mock_instance = MagicMock ()
159+ mock_instance .insert = AsyncMock ()
160+ mock_db_class .return_value = mock_instance
161+
162+ await create_next_triggers (trigger , cron_time )
163+
164+ # Verify trigger_time is a datetime
165+ call_kwargs = mock_db_class .call_args [1 ]
166+ assert isinstance (call_kwargs ['trigger_time' ], datetime )
167+
168+
169+ @pytest .mark .asyncio
170+ async def test_create_next_triggers_creates_multiple_triggers ():
171+ """Test create_next_triggers creates multiple future triggers"""
172+ trigger = MagicMock ()
173+ trigger .expression = "0 */6 * * *" # Every 6 hours
174+ trigger .timezone = "UTC"
175+ trigger .trigger_time = datetime (2025 , 10 , 4 , 0 , 0 , 0 )
176+ trigger .graph_name = "test_graph"
177+ trigger .namespace = "test_namespace"
178+
179+ cron_time = datetime (2025 , 10 , 5 , 0 , 0 , 0 ) # 24 hours later
180+
181+ with patch ('app.tasks.trigger_cron.DatabaseTriggers' ) as mock_db_class :
182+ mock_instance = MagicMock ()
183+ mock_instance .insert = AsyncMock ()
184+ mock_db_class .return_value = mock_instance
185+
186+ await create_next_triggers (trigger , cron_time )
187+
188+ # Should create multiple triggers (every 6 hours until past cron_time)
189+ assert mock_db_class .call_count >= 4 # At least 4 triggers in 24 hours
0 commit comments