-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_settings_application.py
More file actions
330 lines (264 loc) · 14.1 KB
/
Copy pathtest_settings_application.py
File metadata and controls
330 lines (264 loc) · 14.1 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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# -*- coding: utf-8 -*-
"""Regressionstests für DevCenter-Einstellungen."""
import os
import sys
import tempfile
import unittest
import json
from pathlib import Path
from unittest.mock import MagicMock, patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from PySide6.QtCore import QByteArray
from PySide6.QtWidgets import QApplication, QTabWidget
from core.settings_manager import SettingsManager
from core.project_manager import ProjectManager
from gui.dialogs.settings_dialog import SettingsDialog
from gui.main_window import MainWindow
from modules.editor.code_editor import CodeEditor
from modules.filemanager.profiler_bridge import ProfilerBridge
class DevCenterSettingsTests(unittest.TestCase):
"""Tests für die Anwendung von Editor-Einstellungen."""
@classmethod
def setUpClass(cls):
cls.app = QApplication.instance() or QApplication([])
def setUp(self):
self.keyring_patcher = patch("core.settings_manager.keyring")
self.keyring_mock = self.keyring_patcher.start()
self.keyring_mock.get_password.return_value = None
self.addCleanup(self.keyring_patcher.stop)
def _temp_settings(self):
temp_dir = tempfile.TemporaryDirectory()
self.addCleanup(temp_dir.cleanup)
settings_path = Path(temp_dir.name) / "settings.json"
return SettingsManager(str(settings_path))
def test_settings_dialog_saves_highlight_current_line(self):
settings = self._temp_settings()
dialog = SettingsDialog(settings)
dialog.highlight_line.setChecked(False)
dialog._save_settings()
self.assertFalse(settings.get("editor.highlight_current_line"))
def test_settings_dialog_browse_buttons_expose_accessible_context(self):
settings = self._temp_settings()
dialog = SettingsDialog(settings)
browse_buttons = [
(
dialog.pyinstaller_browse_btn,
"PyInstaller-Datei auswählen",
"Öffnet die Dateiauswahl für den PyInstaller-Pfad.",
),
(
dialog.output_dir_browse_btn,
"Ausgabeverzeichnis auswählen",
"Öffnet die Ordnerauswahl für das Standard-Ausgabeverzeichnis.",
),
(
dialog.backup_browse_btn,
"Backup-Verzeichnis auswählen",
"Öffnet die Ordnerauswahl für den Backup-Pfad.",
),
]
for button, accessible_name, description in browse_buttons:
self.assertEqual(button.text(), "...")
self.assertEqual(button.toolTip(), accessible_name)
self.assertEqual(button.accessibleName(), accessible_name)
self.assertEqual(button.accessibleDescription(), description)
def test_main_window_applies_editor_settings_to_open_tabs(self):
settings = self._temp_settings()
settings.set("editor.font_family", "Courier New")
settings.set("editor.font_size", 14)
settings.set("editor.tab_size", 2)
settings.set("editor.show_line_numbers", False)
settings.set("editor.auto_complete", False)
settings.set("editor.highlight_current_line", False)
settings.set("ai.api_key", "secret-token")
window = MainWindow.__new__(MainWindow)
window.settings = settings
window.ai_service = MagicMock()
window.editor_tabs = QTabWidget()
editor = CodeEditor()
window.editor_tabs.addTab(editor, "scratch.py")
window._apply_settings()
window.ai_service.set_api_key.assert_called_once_with("secret-token")
self.assertEqual(editor.font().family(), "Courier New")
self.assertEqual(editor.font().pointSize(), 14)
self.assertEqual(editor.tab_size, 2)
self.assertEqual(editor.line_number_area_width(), 0)
self.assertFalse(editor.autocomplete_enabled)
self.assertFalse(editor.highlight_current_line_enabled)
self.assertEqual(editor.extraSelections(), [])
def test_api_key_is_saved_to_keyring_but_not_settings_json(self):
with patch("core.settings_manager.keyring") as keyring_mock:
keyring_mock.get_password.return_value = None
settings = self._temp_settings()
self.assertTrue(settings.set("ai.api_key", "secret-token"))
payload = json.loads(Path(settings.settings_path).read_text(encoding="utf-8"))
self.assertNotIn("api_key", payload["ai"])
keyring_mock.set_password.assert_called_once_with(
"DevCenter", "anthropic_api_key", "secret-token"
)
self.assertEqual(settings.get("ai.api_key"), "secret-token")
def test_legacy_plaintext_api_key_is_migrated_and_removed(self):
with tempfile.TemporaryDirectory() as temp_dir:
settings_path = Path(temp_dir) / "settings.json"
settings_path.write_text(
json.dumps({"ai": {"api_key": "legacy-secret", "model": "Claude Sonnet"}}),
encoding="utf-8",
)
with patch("core.settings_manager.keyring") as keyring_mock:
keyring_mock.get_password.return_value = None
settings = SettingsManager(str(settings_path))
payload = json.loads(settings_path.read_text(encoding="utf-8"))
self.assertNotIn("api_key", payload["ai"])
keyring_mock.set_password.assert_called_once_with(
"DevCenter", "anthropic_api_key", "legacy-secret"
)
self.assertEqual(settings.get("ai.api_key"), "legacy-secret")
def test_legacy_key_is_not_duplicated_when_json_cleanup_fails(self):
with tempfile.TemporaryDirectory() as temp_dir:
settings_path = Path(temp_dir) / "settings.json"
settings_path.write_text(
json.dumps({"ai": {"api_key": "legacy-secret"}}),
encoding="utf-8",
)
with patch("core.settings_manager.keyring") as keyring_mock, patch.object(
SettingsManager, "_save", return_value=False
):
keyring_mock.get_password.return_value = None
settings = SettingsManager(str(settings_path))
keyring_mock.set_password.assert_not_called()
self.assertEqual(settings.get("ai.api_key"), "legacy-secret")
self.assertIn("legacy-secret", settings_path.read_text(encoding="utf-8"))
def test_settings_export_never_contains_api_key(self):
with patch("core.settings_manager.keyring") as keyring_mock:
keyring_mock.get_password.return_value = None
settings = self._temp_settings()
self.assertTrue(settings.set("ai.api_key", "secret-token"))
export_path = Path(settings.settings_path).with_name("settings-export.json")
self.assertTrue(settings.export_settings(str(export_path)))
exported = export_path.read_text(encoding="utf-8")
self.assertNotIn("secret-token", exported)
self.assertNotIn("api_key", json.loads(exported)["ai"])
def test_keyring_failure_does_not_persist_or_accept_api_key(self):
with patch("core.settings_manager.keyring") as keyring_mock:
keyring_mock.get_password.return_value = None
keyring_mock.set_password.side_effect = RuntimeError("backend unavailable")
settings = self._temp_settings()
self.assertFalse(settings.set("ai.api_key", "secret-token"))
payload = Path(settings.settings_path).read_text(encoding="utf-8")
self.assertNotIn("secret-token", payload)
self.assertEqual(settings.get("ai.api_key"), "")
def test_reset_fails_closed_when_keyring_delete_fails(self):
settings = self._temp_settings()
settings.settings.ai.api_key = "secret-token"
with patch("core.settings_manager.keyring.delete_password") as delete_password:
delete_password.side_effect = RuntimeError("backend unavailable")
self.assertFalse(settings.reset_to_defaults("ai"))
self.assertEqual(settings.get("ai.api_key"), "secret-token")
def test_unchanged_empty_api_key_does_not_require_keyring_write(self):
settings = self._temp_settings()
dialog = SettingsDialog(settings)
with patch.object(settings, "set", wraps=settings.set) as settings_set:
dialog._save_settings()
api_key_calls = [
call for call in settings_set.call_args_list if call.args[0] == "ai.api_key"
]
self.assertEqual(api_key_calls, [])
def test_settings_dialog_stays_open_when_keyring_write_fails(self):
settings = self._temp_settings()
dialog = SettingsDialog(settings)
dialog.api_key.setText("secret-token")
original_set = settings.set
def fail_only_for_api_key(key, value, save=True):
if key == "ai.api_key":
return False
return original_set(key, value, save)
with patch.object(settings, "set", side_effect=fail_only_for_api_key), patch(
"gui.dialogs.settings_dialog.QMessageBox.critical"
) as critical_mock:
dialog._save_settings()
critical_mock.assert_called_once()
self.assertEqual(dialog.result(), 0)
def test_code_editor_autocomplete_initialized_without_apply_settings(self):
editor = CodeEditor()
self.assertTrue(hasattr(editor, 'autocomplete_enabled'),
"autocomplete_enabled must be set in __init__, not only in apply_settings")
self.assertTrue(editor.autocomplete_enabled)
def test_window_state_roundtrip_accepts_qbytearray(self):
settings = self._temp_settings()
geometry = QByteArray(b"geometry-state")
state = QByteArray(b"window-state")
settings.save_window_state(geometry, state)
restored_geometry, restored_state = settings.restore_window_state()
self.assertEqual(bytes(restored_geometry), b"geometry-state")
self.assertEqual(bytes(restored_state), b"window-state")
def test_settings_manager_uses_xdg_config_path_on_posix(self):
with tempfile.TemporaryDirectory() as temp_home, tempfile.TemporaryDirectory() as temp_xdg:
with unittest.mock.patch("core.app_paths.sys.platform", "linux"), unittest.mock.patch.dict(
os.environ,
{"HOME": temp_home, "XDG_CONFIG_HOME": temp_xdg},
clear=False,
):
settings = SettingsManager()
self.addCleanup(Path(settings.settings_path).unlink, missing_ok=True)
expected = Path(temp_xdg) / "DevCenter" / "settings.json"
self.assertEqual(Path(settings.settings_path), expected)
self.assertTrue(expected.exists())
def test_project_manager_uses_xdg_config_path_on_posix(self):
with tempfile.TemporaryDirectory() as temp_home, tempfile.TemporaryDirectory() as temp_xdg:
with unittest.mock.patch("core.app_paths.sys.platform", "linux"), unittest.mock.patch.dict(
os.environ,
{"HOME": temp_home, "XDG_CONFIG_HOME": temp_xdg},
clear=False,
):
manager = ProjectManager()
expected = Path(temp_xdg) / "DevCenter" / "settings.json"
self.assertEqual(Path(manager.settings_path), expected)
def test_profiler_bridge_uses_xdg_index_path_on_posix(self):
with tempfile.TemporaryDirectory() as temp_home, tempfile.TemporaryDirectory() as temp_xdg:
with unittest.mock.patch("core.app_paths.sys.platform", "linux"), unittest.mock.patch.dict(
os.environ,
{"HOME": temp_home, "XDG_CONFIG_HOME": temp_xdg},
clear=False,
):
bridge = ProfilerBridge()
expected = Path(temp_xdg) / "DevCenter" / "file_index.db"
self.assertEqual(Path(bridge.db_path), expected)
self.assertTrue(expected.exists())
def test_import_settings_restores_general_and_top_level_settings(self):
"""Regression: import_settings() muss language, check_updates, telemetry_enabled und window_state übernehmen."""
with tempfile.TemporaryDirectory() as temp_dir:
source_path = Path(temp_dir) / "source_settings.json"
target_path = Path(temp_dir) / "target_settings.json"
export_path = Path(temp_dir) / "export.json"
source = SettingsManager(str(source_path))
source.set("language", "en")
source.set("check_updates", False)
source.set("telemetry_enabled", True)
source.save_window_state(b"geom-data", b"state-data")
source.set("appearance.theme", "light")
self.assertTrue(source.export_settings(str(export_path)))
target = SettingsManager(str(target_path))
self.assertEqual(target.get("language"), "de")
self.assertTrue(target.get("check_updates"))
self.assertFalse(target.get("telemetry_enabled"))
theme_signals = []
target.theme_changed.connect(lambda t: theme_signals.append(t))
self.assertTrue(target.import_settings(str(export_path)))
self.assertEqual(target.get("language"), "en")
self.assertFalse(target.get("check_updates"))
self.assertTrue(target.get("telemetry_enabled"))
geom, state = target.restore_window_state()
self.assertEqual(bytes(geom), b"geom-data")
self.assertEqual(bytes(state), b"state-data")
self.assertEqual(target.get("appearance.theme"), "light")
self.assertIn("light", theme_signals)
def test_reset_to_defaults_clears_extra_settings(self):
"""Regression: reset_to_defaults(category=None) muss auch _extra_settings leeren."""
settings = self._temp_settings()
settings.set("custom.plugin_field", "value123")
self.assertEqual(settings.get("custom.plugin_field"), "value123")
self.assertTrue(settings.reset_to_defaults())
self.assertIsNone(settings.get("custom.plugin_field"))
if __name__ == "__main__":
unittest.main(verbosity=2)