Coverage for ivatar / test_no_opentelemetry.py: 95%

156 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-03 00:09 +0000

1""" 

2Tests to verify the application works properly without OpenTelemetry packages installed. 

3 

4This test simulates the ImportError scenario by mocking the import failure 

5and ensures all functionality continues to work normally. 

6""" 

7 

8import sys 

9import unittest 

10from unittest.mock import patch 

11from io import BytesIO 

12 

13from django.test import TestCase, RequestFactory, Client 

14from django.contrib.auth.models import User 

15from django.core.files.uploadedfile import SimpleUploadedFile 

16from django.urls import reverse 

17from PIL import Image 

18 

19 

20class NoOpenTelemetryTestCase(TestCase): 

21 """Test application functionality when OpenTelemetry is not available""" 

22 

23 def setUp(self): 

24 self.factory = RequestFactory() 

25 self.client = Client() 

26 

27 # Create test user 

28 self.user = User.objects.create_user( 

29 username="testuser", email="test@example.com", password="testpass123" 

30 ) 

31 

32 # Store original modules for restoration 

33 self.original_modules = {} 

34 for module_name in list(sys.modules.keys()): 

35 if "telemetry" in module_name: 

36 self.original_modules[module_name] = sys.modules[module_name] 

37 

38 def tearDown(self): 

39 """Restore original module state to prevent test isolation issues""" 

40 # Remove any modules that were added during testing 

41 modules_to_remove = [k for k in sys.modules.keys() if "telemetry" in k] 

42 for module in modules_to_remove: 

43 if module in sys.modules: 

44 del sys.modules[module] 

45 

46 # Restore original modules 

47 for module_name, module in self.original_modules.items(): 

48 sys.modules[module_name] = module 

49 

50 # Force reload of telemetry_utils to restore proper state 

51 if "ivatar.telemetry_utils" in self.original_modules: 

52 import importlib 

53 

54 importlib.reload(self.original_modules["ivatar.telemetry_utils"]) 

55 

56 def _mock_import_error(self, name, *args, **kwargs): 

57 """Mock function to simulate ImportError for OpenTelemetry packages""" 

58 if "opentelemetry" in name: 

59 raise ImportError(f"No module named '{name}'") 

60 return self._original_import(name, *args, **kwargs) 

61 

62 def _create_test_image(self, format="PNG", size=(100, 100)): 

63 """Create a test image for upload testing""" 

64 image = Image.new("RGB", size, color="red") 

65 image_io = BytesIO() 

66 image.save(image_io, format=format) 

67 image_io.seek(0) 

68 return image_io 

69 

70 def test_telemetry_utils_without_opentelemetry(self): 

71 """Test that telemetry_utils works when OpenTelemetry is not installed""" 

72 # Create a mock module that simulates ImportError for OpenTelemetry 

73 original_import = __builtins__["__import__"] 

74 

75 def mock_import(name, *args, **kwargs): 

76 if "opentelemetry" in name: 

77 raise ImportError(f"No module named '{name}'") 

78 return original_import(name, *args, **kwargs) 

79 

80 # Patch the import and force module reload 

81 with patch("builtins.__import__", side_effect=mock_import): 

82 # Remove from cache to force reimport 

83 modules_to_remove = [k for k in sys.modules.keys() if "telemetry" in k] 

84 for module in modules_to_remove: 

85 if module in sys.modules: 

86 del sys.modules[module] 

87 

88 # Import should trigger the ImportError path 

89 import importlib 

90 import ivatar.telemetry_utils 

91 

92 importlib.reload(ivatar.telemetry_utils) 

93 

94 from ivatar.telemetry_utils import ( 

95 get_telemetry_decorators, 

96 get_telemetry_metrics, 

97 is_telemetry_available, 

98 ) 

99 

100 # Should indicate telemetry is not available 

101 self.assertFalse(is_telemetry_available()) 

102 

103 # Should get no-op decorators 

104 trace_avatar, trace_file, trace_auth = get_telemetry_decorators() 

105 

106 # Test decorators work as no-op 

107 @trace_avatar("test") 

108 def test_func(): 

109 return "success" 

110 

111 self.assertEqual(test_func(), "success") 

112 

113 # Should get no-op metrics 

114 metrics = get_telemetry_metrics() 

115 

116 # These should not raise exceptions 

117 metrics.record_avatar_generated(size="80", format_type="png", source="test") 

118 metrics.record_cache_hit(size="80", format_type="png") 

119 metrics.record_external_request("test", 200) 

120 metrics.record_file_upload(1024, "image/png", True) 

121 

122 @patch.dict( 

123 "sys.modules", 

124 { 

125 "opentelemetry": None, 

126 "opentelemetry.trace": None, 

127 "opentelemetry.metrics": None, 

128 }, 

129 ) 

130 def test_views_work_without_opentelemetry(self): 

131 """Test that views work when OpenTelemetry is not installed""" 

132 # Force reimport to trigger ImportError path 

133 modules_to_reload = [ 

134 "ivatar.telemetry_utils", 

135 "ivatar.views", 

136 "ivatar.ivataraccount.views", 

137 ] 

138 

139 for module in modules_to_reload: 

140 if module in sys.modules: 

141 del sys.modules[module] 

142 

143 # Import views - this should work without OpenTelemetry 

144 from ivatar.views import AvatarImageView 

145 from ivatar.ivataraccount.views import UploadPhotoView 

146 

147 # Create instances - should not raise exceptions 

148 avatar_view = AvatarImageView() 

149 upload_view = UploadPhotoView() 

150 

151 # Views should have the no-op metrics 

152 self.assertTrue(hasattr(avatar_view, "__class__")) 

153 self.assertTrue(hasattr(upload_view, "__class__")) 

154 

155 def test_avatar_generation_without_opentelemetry(self): 

156 """Test avatar generation works without OpenTelemetry""" 

157 # Test default avatar generation (should work without telemetry) 

158 response = self.client.get("/avatar/nonexistent@example.com?d=identicon&s=80") 

159 

160 # Should get a redirect or image response, not an error 

161 self.assertIn(response.status_code, [200, 302, 404]) 

162 

163 def test_file_upload_without_opentelemetry(self): 

164 """Test file upload works without OpenTelemetry""" 

165 # Login user 

166 self.client.login(username="testuser", password="testpass123") 

167 

168 # Create test image 

169 test_image = self._create_test_image() 

170 uploaded_file = SimpleUploadedFile( 

171 "test.png", test_image.getvalue(), content_type="image/png" 

172 ) 

173 

174 # Test upload (should work without telemetry) 

175 response = self.client.post( 

176 reverse("upload_photo"), {"photo": uploaded_file}, follow=True 

177 ) 

178 

179 # Should not get a server error 

180 self.assertNotEqual(response.status_code, 500) 

181 

182 def test_authentication_without_opentelemetry(self): 

183 """Test authentication works without OpenTelemetry""" 

184 # Test login page access 

185 response = self.client.get(reverse("login")) 

186 self.assertEqual(response.status_code, 200) 

187 

188 # Test login submission 

189 response = self.client.post( 

190 reverse("login"), {"username": "testuser", "password": "testpass123"} 

191 ) 

192 

193 # Should not get a server error 

194 self.assertNotEqual(response.status_code, 500) 

195 

196 def test_user_registration_without_opentelemetry(self): 

197 """Test user registration works without OpenTelemetry""" 

198 # Check if the 'new' URL exists, skip if not 

199 try: 

200 url = reverse("new") 

201 except Exception: 

202 # URL doesn't exist, skip this test 

203 self.skipTest("User registration URL 'new' not found") 

204 

205 response = self.client.post( 

206 url, 

207 { 

208 "username": "newuser", 

209 "password1": "newpass123", 

210 "password2": "newpass123", 

211 }, 

212 ) 

213 

214 # Should not get a server error 

215 self.assertNotEqual(response.status_code, 500) 

216 

217 @patch.dict( 

218 "sys.modules", 

219 { 

220 "opentelemetry": None, 

221 "opentelemetry.trace": None, 

222 "opentelemetry.metrics": None, 

223 }, 

224 ) 

225 def test_decorated_functions_work_without_opentelemetry(self): 

226 """Test that decorated functions work when OpenTelemetry is not available""" 

227 # Force reimport to get no-op decorators 

228 if "ivatar.telemetry_utils" in sys.modules: 

229 del sys.modules["ivatar.telemetry_utils"] 

230 

231 from ivatar.telemetry_utils import ( 

232 trace_avatar_operation, 

233 trace_file_upload, 

234 trace_authentication, 

235 ) 

236 

237 # Test each decorator type 

238 @trace_avatar_operation("test_avatar") 

239 def avatar_function(): 

240 return "avatar_success" 

241 

242 @trace_file_upload("test_upload") 

243 def upload_function(): 

244 return "upload_success" 

245 

246 @trace_authentication("test_auth") 

247 def auth_function(): 

248 return "auth_success" 

249 

250 # All should work normally 

251 self.assertEqual(avatar_function(), "avatar_success") 

252 self.assertEqual(upload_function(), "upload_success") 

253 self.assertEqual(auth_function(), "auth_success") 

254 

255 def test_metrics_recording_without_opentelemetry(self): 

256 """Test that metrics recording works (as no-op) without OpenTelemetry""" 

257 # Test the no-op metrics class directly 

258 from ivatar.telemetry_utils import NoOpMetrics 

259 

260 metrics = NoOpMetrics() 

261 

262 # These should all work without exceptions 

263 metrics.record_avatar_generated(size="80", format_type="png", source="test") 

264 metrics.record_avatar_request(size="80", format_type="png") 

265 metrics.record_cache_hit(size="80", format_type="png") 

266 metrics.record_cache_miss(size="80", format_type="png") 

267 metrics.record_external_request("gravatar", 200) 

268 metrics.record_file_upload(1024, "image/png", True) 

269 

270 # Verify it's the no-op implementation 

271 self.assertEqual(metrics.__class__.__name__, "NoOpMetrics") 

272 

273 def test_application_startup_without_opentelemetry(self): 

274 """Test that Django can start without OpenTelemetry packages""" 

275 # This test verifies that the settings.py OpenTelemetry setup 

276 # handles ImportError gracefully 

277 

278 # The fact that this test runs means Django started successfully 

279 # even if OpenTelemetry packages were missing during import 

280 

281 from django.conf import settings 

282 

283 # Django should be configured 

284 self.assertTrue(settings.configured) 

285 

286 # Middleware should be loaded (even if OpenTelemetry middleware failed to load) 

287 self.assertIsInstance(settings.MIDDLEWARE, list) 

288 

289 def test_views_import_safely_without_opentelemetry(self): 

290 """Test that all views can be imported without OpenTelemetry""" 

291 # These imports should not raise ImportError even without OpenTelemetry 

292 try: 

293 from ivatar import views 

294 from ivatar.ivataraccount import views as account_views 

295 

296 # Should be able to access the views 

297 self.assertTrue(hasattr(views, "AvatarImageView")) 

298 self.assertTrue(hasattr(account_views, "UploadPhotoView")) 

299 

300 except ImportError as e: 

301 self.fail(f"Views failed to import without OpenTelemetry: {e}") 

302 

303 def test_middleware_handles_missing_opentelemetry(self): 

304 """Test that middleware handles missing OpenTelemetry gracefully""" 

305 # Test a simple request to ensure middleware doesn't break 

306 response = self.client.get("/") 

307 

308 # Should get some response, not a server error 

309 self.assertNotEqual(response.status_code, 500) 

310 

311 

312class OpenTelemetryFallbackIntegrationTest(TestCase): 

313 """Integration tests for OpenTelemetry fallback behavior""" 

314 

315 def setUp(self): 

316 self.client = Client() 

317 

318 def test_full_avatar_workflow_without_opentelemetry(self): 

319 """Test complete avatar workflow works without OpenTelemetry""" 

320 # Test various avatar generation methods 

321 test_cases = [ 

322 "/avatar/test@example.com?d=identicon&s=80", 

323 "/avatar/test@example.com?d=monsterid&s=80", 

324 "/avatar/test@example.com?d=robohash&s=80", 

325 "/avatar/test@example.com?d=retro&s=80", 

326 "/avatar/test@example.com?d=pagan&s=80", 

327 "/avatar/test@example.com?d=mm&s=80", 

328 ] 

329 

330 for url in test_cases: 

331 with self.subTest(url=url): 

332 response = self.client.get(url) 

333 # Should not get server error 

334 self.assertNotEqual( 

335 response.status_code, 500, f"Server error for {url}" 

336 ) 

337 

338 def test_stats_endpoint_without_opentelemetry(self): 

339 """Test stats endpoint works without OpenTelemetry""" 

340 response = self.client.get("/stats/") 

341 

342 # Should not get server error 

343 self.assertNotEqual(response.status_code, 500) 

344 

345 def test_version_endpoint_without_opentelemetry(self): 

346 """Test version endpoint works without OpenTelemetry""" 

347 response = self.client.get("/version/") 

348 

349 # Should not get server error 

350 self.assertNotEqual(response.status_code, 500) 

351 

352 

353if __name__ == "__main__": 

354 unittest.main()