"""Tests for safe runtime settings .env editing helpers.""" from __future__ import annotations import errno from pathlib import Path import pytest from transcription.config import Settings from transcription.errors import AppError from transcription.ui import runtime_settings_store from transcription.ui.runtime_settings_store import HIDDEN_SETTINGS_CATEGORIES from transcription.ui.runtime_settings_store import RUNTIME_SETTINGS_CATALOG from transcription.ui.runtime_settings_store import SETTINGS_UI_EXCLUDED_FIELDS from transcription.ui.runtime_settings_store import read_runtime_settings_snapshot from transcription.ui.runtime_settings_store import save_runtime_settings from transcription.ui.runtime_settings_store import settings_catalog_field_names def _settings_for_runtime_editing(tmp_path: Path) -> Settings: env_path = tmp_path / ".env" env_path.write_text("OPENROUTER_API_KEY=test-key\n", encoding="utf-8") return Settings(_env_file=env_path, _cli_parse_args=False) def test_runtime_settings_catalog_classifies_all_non_secret_settings_fields(): available_fields = set(Settings.model_fields) expected = available_fields - set(SETTINGS_UI_EXCLUDED_FIELDS) assert settings_catalog_field_names() == expected assert "openrouter_api_key" not in settings_catalog_field_names() def test_hidden_settings_categories_list_secret_and_high_risk_env_keys(): hidden_keys = {key for category in HIDDEN_SETTINGS_CATEGORIES for key in category.env_keys} assert "OPENROUTER_API_KEY" in hidden_keys assert "DATABASE__PASSWORD" in hidden_keys assert "DATABASE__DRIVER" in hidden_keys assert "DATABASE__HOST" in hidden_keys assert "POSTGRES_DB" in hidden_keys assert "POSTGRES_USER" in hidden_keys assert "POSTGRES_PASSWORD" in hidden_keys assert "CLOUDFLARE_TUNNEL_TOKEN" in hidden_keys assert "BACKUP_DIR" in hidden_keys def test_runtime_settings_snapshot_includes_catalog_fields(tmp_path: Path): settings = _settings_for_runtime_editing(tmp_path) snapshot = read_runtime_settings_snapshot(settings=settings, env_file_path=tmp_path / ".env") assert snapshot.env_file_path == tmp_path / ".env" assert len(snapshot.fields) == len(RUNTIME_SETTINGS_CATALOG) assert any(field.field_name == "port" for field in snapshot.fields) assert any(field.field_name == "provider" for field in snapshot.fields) def test_save_runtime_settings_writes_allowed_updates(tmp_path: Path): settings = _settings_for_runtime_editing(tmp_path) env_path = tmp_path / ".env" env_path.write_text( "# sample env\nOPENROUTER_API_KEY=test-key\nPORT=8000\nLOG_LEVEL=info\n", encoding="utf-8", ) snapshot = save_runtime_settings( settings=settings, updates={ "port": "9001", "log_level": "debug", "reload": True, }, env_file_path=env_path, ) updated = env_path.read_text(encoding="utf-8") assert "PORT=9001" in updated assert "LOG_LEVEL=debug" in updated assert "RELOAD=true" in updated assert any(field.field_name == "port" and str(field.value) == "9001" for field in snapshot.fields) def test_save_runtime_settings_rejects_invalid_values(tmp_path: Path): settings = _settings_for_runtime_editing(tmp_path) env_path = tmp_path / ".env" with pytest.raises(AppError): save_runtime_settings( settings=settings, updates={"port": "not-a-number"}, env_file_path=env_path, ) def test_runtime_settings_uses_override_env_file(tmp_path: Path, monkeypatch): settings = _settings_for_runtime_editing(tmp_path) target = tmp_path / ".env.production" target.write_text("OPENROUTER_API_KEY=test-key\nPORT=8000\n", encoding="utf-8") monkeypatch.setenv("RUNTIME_SETTINGS_ENV_FILE", str(target)) snapshot = save_runtime_settings( settings=settings, updates={"port": "9001"}, ) assert snapshot.env_file_path == target assert "PORT=9001" in target.read_text(encoding="utf-8") def test_save_runtime_settings_surfaces_unreadable_target(tmp_path: Path, monkeypatch): settings = _settings_for_runtime_editing(tmp_path) env_path = tmp_path / ".env" def _raise_read_error(_self, encoding="utf-8"): _ = encoding raise OSError("denied") monkeypatch.setattr(Path, "read_text", _raise_read_error) with pytest.raises(AppError) as exc: save_runtime_settings(settings=settings, updates={"port": "9001"}, env_file_path=env_path) assert exc.value.message == "Runtime settings file is unreadable." def test_save_runtime_settings_surfaces_unwritable_target(tmp_path: Path, monkeypatch): settings = _settings_for_runtime_editing(tmp_path) env_path = tmp_path / ".env" def _raise_write_error(*args, **kwargs): _ = args, kwargs raise OSError("denied") monkeypatch.setattr(runtime_settings_store, "_write_env_lines_atomic", _raise_write_error) with pytest.raises(AppError) as exc: save_runtime_settings(settings=settings, updates={"port": "9001"}, env_file_path=env_path) assert exc.value.message == "Runtime settings file is not writable." def test_save_runtime_settings_falls_back_when_atomic_replace_is_unavailable(tmp_path: Path, monkeypatch): settings = _settings_for_runtime_editing(tmp_path) env_path = tmp_path / ".env.production" env_path.write_text("OPENROUTER_API_KEY=test-key\nPORT=8000\n", encoding="utf-8") def _replace_cross_device(_self: Path, _target: Path) -> Path: raise OSError(errno.EXDEV, "Invalid cross-device link") monkeypatch.setattr(Path, "replace", _replace_cross_device) snapshot = save_runtime_settings(settings=settings, updates={"port": "9001"}, env_file_path=env_path) assert snapshot.env_file_path == env_path assert "PORT=9001" in env_path.read_text(encoding="utf-8")