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