-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_v1_6_features.py
More file actions
205 lines (166 loc) · 8.94 KB
/
Copy pathtest_v1_6_features.py
File metadata and controls
205 lines (166 loc) · 8.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import unittest
from unittest.mock import MagicMock, patch
import json
import os
import re
from datetime import datetime, timedelta
from main import WatcherService
from config import Config
from models import UpdateStatus, ContainerUpdateInfo
from exceptions import ConfigurationError
class TestV1_6Features(unittest.TestCase):
def setUp(self):
# Mock docker environment
self.mock_docker_client = MagicMock()
self.patcher = patch('docker.from_env', return_value=self.mock_docker_client)
self.patcher.start()
# Reset env vars for each test
if 'NOTIFY_SUMMARY_STRATEGY' in os.environ: del os.environ['NOTIFY_SUMMARY_STRATEGY']
if 'NOTIFY_UPDATES_AVAILABLE' in os.environ: del os.environ['NOTIFY_UPDATES_AVAILABLE']
if 'TELEGRAM_BOT_TOKEN' in os.environ: del os.environ['TELEGRAM_BOT_TOKEN']
if 'TELEGRAM_CHAT_ID' in os.environ: del os.environ['TELEGRAM_CHAT_ID']
if 'DISCORD_WEBHOOK_URL' in os.environ: del os.environ['DISCORD_WEBHOOK_URL']
if 'EXCLUDE_CONTAINER_REGEX' in os.environ: del os.environ['EXCLUDE_CONTAINER_REGEX']
def tearDown(self):
self.patcher.stop()
if os.path.exists("test_journal.json"):
os.remove("test_journal.json")
def test_start_without_notifications(self):
"""Watcher should start cleanly if no notifier is configured."""
with patch.dict(os.environ, {"DISCORD_WEBHOOK_URL": ""}):
service = WatcherService()
# NoopNotifier doesn't have a __class__ name easily checked via mock,
# but we can check if it's not a DiscordNotifier
from noop_notifier import NoopNotifier
self.assertIsInstance(service.notifier, NoopNotifier)
def test_fail_fast_incomplete_telegram(self):
"""Should fail if only part of Telegram config is provided."""
with patch.dict(os.environ, {"TELEGRAM_BOT_TOKEN": "123"}):
with self.assertRaises(ConfigurationError) as cm:
Config()
self.assertIn("Both TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID must be set", str(cm.exception))
def test_regex_exclude(self):
"""Check if containers are correctly filtered by regex."""
with patch.dict(os.environ, {"EXCLUDE_CONTAINER_REGEX": "^test_.*"}):
config = Config()
mock_c1 = MagicMock()
mock_c1.name = "test_app"
mock_c2 = MagicMock()
mock_c2.name = "prod_app"
self.mock_docker_client.containers.list.return_value = [mock_c1, mock_c2]
from docker_handler import DockerHandler
handler = DockerHandler(self.mock_docker_client, config)
# Mock get_image_ref to return something for both
handler.get_image_ref = MagicMock(return_value="image:latest")
auto, monitor = handler.get_watched_containers()
names = [c.name for c in auto] + [c.name for c in monitor]
self.assertIn("prod_app", names)
self.assertNotIn("test_app", names)
def test_cooldown_behavior(self):
"""Verify that containers enter and exit cooldown correctly."""
with patch.dict(os.environ, {"FAILURE_COOLDOWN_SECONDS": "10"}):
service = WatcherService()
name = "fail_app"
# Initial state
self.assertFalse(service._is_in_cooldown(name))
# Record failure
service._record_failure(name)
self.assertTrue(service._is_in_cooldown(name))
# Wait for cooldown to expire (simulated)
service.failure_tracker[name]["cooldown_until"] = datetime.now() - timedelta(seconds=1)
self.assertFalse(service._is_in_cooldown(name))
# Successful check should clear it
service._record_failure(name)
self.assertTrue(service._is_in_cooldown(name))
# Mock successful process
mock_container = MagicMock()
mock_container.name = name
service.docker.check_for_update = MagicMock(return_value=(UpdateStatus.NO_UPDATE, "old", "old"))
service.process_container(mock_container, True)
self.assertNotIn(name, service.failure_tracker)
def test_journal_writing(self):
"""Check if journal file is created and contains expected data."""
os.environ["JOURNAL_PATH"] = "test_journal.json"
service = WatcherService()
summary = {"updated": ["app1"], "failed": [], "rolled_back": [], "reported": [], "skipped": []}
info = ContainerUpdateInfo("app1", "id1", UpdateStatus.UPDATED)
info.duration_sec = 5.5
service.journal.record_cycle(1, "LIVE", summary, [info], 10.0)
self.assertTrue(os.path.exists("test_journal.json"))
with open("test_journal.json", "r") as f:
data = json.load(f)
self.assertEqual(len(data), 1)
self.assertEqual(data[0]["summary"]["updated"], ["app1"])
self.assertEqual(data[0]["events"][0]["name"], "app1")
def test_summary_strategies(self):
"""Test always, on_change, and on_error strategies."""
# Setup
service = WatcherService()
service.notifier.notify_summary_report = MagicMock()
# 1. Strategy: on_error, but only 'reported' exists -> No notification
service.config.notify_summary_strategy = "on_error"
summary = {"updated": [], "failed": [], "rolled_back": [], "reported": ["app1"], "skipped": []}
# Need to patch run_cycle's container list
service.docker.get_watched_containers = MagicMock(return_value=([], []))
# Manual trigger of end-of-cycle logic logic
def check_strategy(summ):
should = False
strat = service.config.notify_summary_strategy
has_errors = len(summ["failed"]) > 0 or len(summ["rolled_back"]) > 0
has_actions = len(summ["updated"]) > 0 or has_errors
if strat == "always": should = True
elif strat == "on_error": should = has_errors
elif strat == "on_change": should = has_actions
return should
self.assertFalse(check_strategy(summary))
# 2. Strategy: on_change, updated exists -> Yes notification
service.config.notify_summary_strategy = "on_change"
summary["updated"] = ["app2"]
self.assertTrue(check_strategy(summary))
# 3. Strategy: on_change, only reported exists -> No notification (Decision: change = action taken)
summary["updated"] = []
summary["reported"] = ["app3"]
self.assertFalse(check_strategy(summary))
def test_run_cycle_no_notifications(self):
"""A full cycle should run without errors even if notifications are disabled."""
with patch.dict(os.environ, {"DISCORD_WEBHOOK_URL": ""}):
service = WatcherService()
service.docker.get_watched_containers = MagicMock(return_value=([], []))
# Should not crash
service.run_cycle()
def test_cycle_with_cooldown(self):
"""Containers in cooldown should be skipped in the cycle."""
service = WatcherService()
mock_c = MagicMock()
mock_c.name = "cool_app"
mock_c.id = "id123"
service.docker.get_watched_containers = MagicMock(return_value=([mock_c], []))
service.process_container = MagicMock()
# Put in cooldown
service._record_failure("cool_app")
service.run_cycle()
# process_container should NOT have been called
service.process_container.assert_not_called()
def test_journal_io_error_handling(self):
"""Watcher should remain stable if the journal file is not writable."""
with patch("builtins.open", side_effect=IOError("Permission denied")):
service = WatcherService()
# Try to record a cycle - should log error but not crash
service.journal.record_cycle(1, "LIVE", {}, [], 1.0)
self.assertEqual(len(service.journal._history), 1)
def test_max_updates_limit(self):
"""Cycle should respect MAX_UPDATES_PER_CYCLE."""
service = WatcherService()
service.config.max_updates_per_cycle = 1
c1 = MagicMock(); c1.name = "app1"
c2 = MagicMock(); c2.name = "app2"
service.docker.get_watched_containers = MagicMock(return_value=([c1, c2], []))
# Mock process_container to return UPDATED for the first one
def mock_process(c, auto_update=True):
return ContainerUpdateInfo(c.name, "id", UpdateStatus.UPDATED)
service.process_container = MagicMock(side_effect=mock_process)
service.run_cycle()
# Should only have called process_container once for auto-update
self.assertEqual(service.process_container.call_count, 1)
if __name__ == '__main__':
unittest.main()