diff --git a/src/mini_eq/core.py b/src/mini_eq/core.py index 9948405..f3672fd 100644 --- a/src/mini_eq/core.py +++ b/src/mini_eq/core.py @@ -313,6 +313,22 @@ def preset_path_for_name(name: str) -> Path: return ensure_preset_storage_dir() / f"{preset_name}{PRESET_FILE_SUFFIX}" +def delete_preset_file(name: str) -> None: + preset_name = sanitize_preset_name(name) + if not preset_name: + raise ValueError("preset name is empty") + + storage_dir = ensure_preset_storage_dir() + dir_fd = os.open(storage_dir, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)) + try: + try: + os.unlink(f"{preset_name}{PRESET_FILE_SUFFIX}", dir_fd=dir_fd) + except FileNotFoundError: + return + finally: + os.close(dir_fd) + + def list_preset_names() -> list[str]: names = [ path.stem diff --git a/src/mini_eq/window_presets.py b/src/mini_eq/window_presets.py index 13b442c..9c166d2 100644 --- a/src/mini_eq/window_presets.py +++ b/src/mini_eq/window_presets.py @@ -13,6 +13,7 @@ from .core import ( PRESET_FILE_SUFFIX, PRESET_VERSION, + delete_preset_file, ensure_json_suffix, fader_band_count_for_profile, list_preset_names, @@ -241,9 +242,7 @@ def on_preset_delete_dialog_done( return try: - preset_path = preset_path_for_name(preset_name) - if preset_path.exists(): - preset_path.unlink() + delete_preset_file(preset_name) self.current_preset_name = None self.saved_preset_signature = self.controller.state_signature() self.refresh_preset_list() diff --git a/tests/test_mini_eq_core.py b/tests/test_mini_eq_core.py index fb74965..5fea402 100644 --- a/tests/test_mini_eq_core.py +++ b/tests/test_mini_eq_core.py @@ -59,6 +59,46 @@ def test_preset_roundtrip_and_listing_uses_storage_dir(monkeypatch: pytest.Monke assert core.load_mini_eq_preset_file(core.preset_path_for_name("beta")) == beta_payload +def test_delete_preset_file_removes_only_named_storage_file(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: + storage_dir = tmp_path / "mini-eq-presets" + monkeypatch.setattr(core, "PRESET_STORAGE_DIR", storage_dir) + payload = {"version": core.PRESET_VERSION, "name": "Alpha", "bands": []} + + preset_path = core.preset_path_for_name("Alpha") + core.write_mini_eq_preset_file(preset_path, payload) + + core.delete_preset_file("Alpha") + + assert not preset_path.exists() + + +def test_delete_preset_file_ignores_missing_file(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: + monkeypatch.setattr(core, "PRESET_STORAGE_DIR", tmp_path / "mini-eq-presets") + + core.delete_preset_file("Missing") + + +def test_delete_preset_file_rejects_empty_name(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: + monkeypatch.setattr(core, "PRESET_STORAGE_DIR", tmp_path / "mini-eq-presets") + + with pytest.raises(ValueError, match="preset name is empty"): + core.delete_preset_file("../") + + +def test_delete_preset_file_uses_sanitized_basename(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: + storage_dir = tmp_path / "mini-eq-presets" + monkeypatch.setattr(core, "PRESET_STORAGE_DIR", storage_dir) + outside_path = tmp_path / "outside.json" + sanitized_path = storage_dir / "outside.json" + outside_path.write_text("outside", encoding="utf-8") + core.write_mini_eq_preset_file(sanitized_path, {"version": core.PRESET_VERSION, "name": "outside", "bands": []}) + + core.delete_preset_file("../outside") + + assert outside_path.read_text(encoding="utf-8") == "outside" + assert not sanitized_path.exists() + + def test_load_mini_eq_preset_file_rejects_invalid_shape(tmp_path) -> None: preset_path = tmp_path / "broken.json" preset_path.write_text('{"version": 1, "bands": "nope"}', encoding="utf-8")