|
1 | 1 | import os |
| 2 | +import threading |
2 | 3 |
|
3 | 4 | import pytest |
4 | 5 | from datetime import datetime |
|
8 | 9 | from django.core.exceptions import ImproperlyConfigured |
9 | 10 | from django.db import connections |
10 | 11 |
|
| 12 | + |
11 | 13 | try: |
12 | 14 | from django.urls import reverse |
13 | 15 | except ImportError: |
|
17 | 19 |
|
18 | 20 | from sentry_sdk import start_transaction |
19 | 21 | from sentry_sdk.consts import SPANDATA |
| 22 | +from sentry_sdk.integrations import django as django_integration |
20 | 23 | from sentry_sdk.integrations.django import ( |
21 | 24 | DjangoIntegration, |
22 | 25 | _set_db_data, |
23 | 26 | _cached_db_configs, |
| 27 | + _cache_database_configurations, |
24 | 28 | ) |
25 | 29 | from sentry_sdk.tracing_utils import record_sql_queries |
26 | 30 |
|
@@ -986,3 +990,153 @@ def test_set_db_data_empty_cached_values(sentry_init): |
986 | 990 |
|
987 | 991 | for call in not_expected_calls: |
988 | 992 | assert call not in span.set_data.call_args_list |
| 993 | + |
| 994 | + |
| 995 | +def test_cache_database_configurations_basic(sentry_init): |
| 996 | + """Test _cache_database_configurations caches Django database settings.""" |
| 997 | + sentry_init(integrations=[DjangoIntegration()]) |
| 998 | + |
| 999 | + _cached_db_configs.clear() |
| 1000 | + django_integration._cache_initialized = False |
| 1001 | + |
| 1002 | + _cache_database_configurations() |
| 1003 | + |
| 1004 | + # Verify cache was populated |
| 1005 | + assert django_integration._cache_initialized is True |
| 1006 | + assert len(_cached_db_configs) > 0 |
| 1007 | + |
| 1008 | + # Verify default database was cached |
| 1009 | + assert "default" in _cached_db_configs |
| 1010 | + default_config = _cached_db_configs["default"] |
| 1011 | + |
| 1012 | + # Check expected keys exist |
| 1013 | + expected_keys = ["db_name", "host", "port", "unix_socket", "engine"] |
| 1014 | + for key in expected_keys: |
| 1015 | + assert key in default_config |
| 1016 | + |
| 1017 | + # Verify the vendor was added from the database wrapper |
| 1018 | + if "vendor" in default_config: |
| 1019 | + assert isinstance(default_config["vendor"], str) |
| 1020 | + |
| 1021 | + |
| 1022 | +def test_cache_database_configurations_idempotent(sentry_init): |
| 1023 | + """Test _cache_database_configurations is idempotent and thread-safe.""" |
| 1024 | + sentry_init(integrations=[DjangoIntegration()]) |
| 1025 | + |
| 1026 | + _cached_db_configs.clear() |
| 1027 | + django_integration._cache_initialized = False |
| 1028 | + |
| 1029 | + _cache_database_configurations() |
| 1030 | + first_call_result = dict(_cached_db_configs) |
| 1031 | + first_call_initialized = django_integration._cache_initialized |
| 1032 | + |
| 1033 | + _cache_database_configurations() |
| 1034 | + second_call_result = dict(_cached_db_configs) |
| 1035 | + second_call_initialized = django_integration._cache_initialized |
| 1036 | + |
| 1037 | + # Verify idempotency |
| 1038 | + assert first_call_initialized is True |
| 1039 | + assert second_call_initialized is True |
| 1040 | + assert first_call_result == second_call_result |
| 1041 | + |
| 1042 | + |
| 1043 | +def test_cache_database_configurations_thread_safety(sentry_init): |
| 1044 | + """Test _cache_database_configurations is thread-safe.""" |
| 1045 | + sentry_init(integrations=[DjangoIntegration()]) |
| 1046 | + |
| 1047 | + _cached_db_configs.clear() |
| 1048 | + django_integration._cache_initialized = False |
| 1049 | + |
| 1050 | + results = [] |
| 1051 | + exceptions = [] |
| 1052 | + |
| 1053 | + def cache_in_thread(): |
| 1054 | + try: |
| 1055 | + _cache_database_configurations() |
| 1056 | + results.append(dict(_cached_db_configs)) |
| 1057 | + except Exception as e: |
| 1058 | + exceptions.append(e) |
| 1059 | + |
| 1060 | + threads = [] |
| 1061 | + for _ in range(5): |
| 1062 | + thread = threading.Thread(target=cache_in_thread) |
| 1063 | + threads.append(thread) |
| 1064 | + |
| 1065 | + for thread in threads: |
| 1066 | + thread.start() |
| 1067 | + |
| 1068 | + for thread in threads: |
| 1069 | + thread.join() |
| 1070 | + |
| 1071 | + assert len(exceptions) == 0, f"Exceptions occurred: {exceptions}" |
| 1072 | + |
| 1073 | + assert len(results) == 5 |
| 1074 | + first_result = results[0] |
| 1075 | + for result in results[1:]: |
| 1076 | + assert result == first_result |
| 1077 | + |
| 1078 | + assert django_integration._cache_initialized is True |
| 1079 | + |
| 1080 | + |
| 1081 | +def test_cache_database_configurations_with_custom_settings(sentry_init): |
| 1082 | + """Test _cache_database_configurations handles custom database settings.""" |
| 1083 | + sentry_init(integrations=[DjangoIntegration()]) |
| 1084 | + |
| 1085 | + # Mock custom database settings |
| 1086 | + with mock.patch("django.conf.settings") as mock_settings: |
| 1087 | + mock_settings.DATABASES = { |
| 1088 | + "custom_db": { |
| 1089 | + "NAME": "test_db", |
| 1090 | + "HOST": "db.example.com", |
| 1091 | + "PORT": 5432, |
| 1092 | + "ENGINE": "django.db.backends.postgresql", |
| 1093 | + "OPTIONS": {"unix_socket": "/tmp/postgres.sock"}, |
| 1094 | + }, |
| 1095 | + "empty_db": {}, # Should be skipped |
| 1096 | + } |
| 1097 | + |
| 1098 | + # Mock connections to avoid actual database access |
| 1099 | + with mock.patch("django.db.connections") as mock_connections: |
| 1100 | + mock_wrapper = mock.Mock() |
| 1101 | + mock_wrapper.vendor = "postgresql" |
| 1102 | + mock_connections.__getitem__.return_value = mock_wrapper |
| 1103 | + |
| 1104 | + # Reset cache and call function |
| 1105 | + _cached_db_configs.clear() |
| 1106 | + django_integration._cache_initialized = False |
| 1107 | + |
| 1108 | + _cache_database_configurations() |
| 1109 | + |
| 1110 | + # Verify custom database was cached correctly |
| 1111 | + assert "custom_db" in _cached_db_configs |
| 1112 | + custom_config = _cached_db_configs["custom_db"] |
| 1113 | + |
| 1114 | + assert custom_config["db_name"] == "test_db" |
| 1115 | + assert custom_config["host"] == "db.example.com" |
| 1116 | + assert custom_config["port"] == 5432 |
| 1117 | + assert custom_config["unix_socket"] == "/tmp/postgres.sock" |
| 1118 | + assert custom_config["engine"] == "django.db.backends.postgresql" |
| 1119 | + assert custom_config["vendor"] == "postgresql" |
| 1120 | + |
| 1121 | + # Verify empty database was skipped |
| 1122 | + assert "empty_db" not in _cached_db_configs |
| 1123 | + |
| 1124 | + |
| 1125 | +def test_cache_database_configurations_exception_handling(sentry_init): |
| 1126 | + """Test _cache_database_configurations handles exceptions gracefully.""" |
| 1127 | + sentry_init(integrations=[DjangoIntegration()]) |
| 1128 | + |
| 1129 | + # Mock settings to raise an exception |
| 1130 | + with mock.patch("django.conf.settings") as mock_settings: |
| 1131 | + mock_settings.DATABASES.items.side_effect = Exception("Settings error") |
| 1132 | + |
| 1133 | + # Reset cache and call function |
| 1134 | + _cached_db_configs.clear() |
| 1135 | + django_integration._cache_initialized = False |
| 1136 | + |
| 1137 | + # Should not raise an exception |
| 1138 | + _cache_database_configurations() |
| 1139 | + |
| 1140 | + # Verify cache was cleared on exception |
| 1141 | + assert _cached_db_configs == {} |
| 1142 | + assert django_integration._cache_initialized is False |
0 commit comments