You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

191 lines
6.8 KiB

from __future__ import annotations
from pathlib import Path
import pytest
from PySide6.QtWidgets import QComboBox, QLineEdit, QMessageBox
from voice_transcriptor.models import AppSettings
from voice_transcriptor.services.credentials import CredentialError
from voice_transcriptor.services.settings import SettingsError
from voice_transcriptor.services.settings import SUPPORTED_MODEL_SUGGESTIONS
from voice_transcriptor.ui.settings_dialog import SettingsDialog
class FakeRepository:
def __init__(self) -> None:
self.saved: list[AppSettings] = []
def save(self, settings: AppSettings) -> None:
self.saved.append(settings)
class FakeCredentials:
def __init__(self, api_key: str | None = None) -> None:
self.api_key = api_key
self.saved_values: list[str] = []
def get_api_key(self) -> str | None:
return self.api_key
def has_api_key(self) -> bool:
return bool(self.api_key)
def set_api_key(self, value: str) -> None:
self.saved_values.append(value)
self.api_key = value
@pytest.fixture
def settings(tmp_path: Path) -> AppSettings:
output_directory = tmp_path / "output"
output_directory.mkdir()
return AppSettings("gpt-4o-transcribe", "pt-BR", output_directory)
def test_populates_fields_from_settings(qtbot, settings: AppSettings) -> None:
dialog = SettingsDialog(settings, FakeRepository(), FakeCredentials())
qtbot.addWidget(dialog)
assert dialog.model_input.text() == settings.model
assert dialog.language_input.text() == settings.language
assert dialog.output_directory_input.text() == str(settings.output_directory)
assert dialog.api_key_input.text() == ""
assert dialog.advanced_panel.isVisible() is False
assert dialog.chunk_duration_input.value() == 900
assert dialog.chunk_overlap_input.value() == 15
assert isinstance(dialog.model_input, QComboBox)
assert dialog.model_input.isEditable()
assert tuple(dialog.model_input.itemText(i) for i in range(dialog.model_input.count())) == SUPPORTED_MODEL_SUGGESTIONS
assert dialog.context_input.toPlainText() == settings.context_vocabulary
def test_advanced_settings_are_saved(qtbot, settings: AppSettings) -> None:
repository = FakeRepository()
dialog = SettingsDialog(settings, repository, FakeCredentials())
qtbot.addWidget(dialog)
dialog.chunk_duration_input.setValue(1200)
dialog.chunk_overlap_input.setValue(20)
dialog.retain_temporary_files_input.setChecked(True)
dialog.save()
assert repository.saved[0].chunk_duration_seconds == 1200
assert repository.saved[0].chunk_overlap_seconds == 20
assert repository.saved[0].retain_temporary_files is True
def test_api_key_input_uses_password_echo_mode(qtbot, settings: AppSettings) -> None:
dialog = SettingsDialog(settings, FakeRepository(), FakeCredentials())
qtbot.addWidget(dialog)
assert dialog.api_key_input.echoMode() is QLineEdit.EchoMode.Password
def test_blank_api_key_preserves_existing_credential(qtbot, settings: AppSettings) -> None:
credentials = FakeCredentials(api_key="stored-token")
repository = FakeRepository()
dialog = SettingsDialog(settings, repository, credentials)
qtbot.addWidget(dialog)
dialog.save()
assert credentials.api_key == "stored-token"
assert credentials.saved_values == []
assert repository.saved == [settings]
def test_save_persists_new_api_key_and_emits_saved_settings(qtbot, settings: AppSettings) -> None:
credentials = FakeCredentials()
repository = FakeRepository()
dialog = SettingsDialog(settings, repository, credentials)
qtbot.addWidget(dialog)
saved_with = qtbot.waitSignal(dialog.settings_saved)
supplied_key = "test-token-for-dialog"
dialog.api_key_input.setText(supplied_key)
dialog.model_input.setText("gpt-4o-mini-transcribe")
dialog.context_input.setPlainText("Pix, Banco do Brasil")
dialog.save()
assert saved_with.args == [
AppSettings("gpt-4o-mini-transcribe", "pt-BR", settings.output_directory, context_vocabulary="Pix, Banco do Brasil")
]
assert credentials.api_key == supplied_key
assert repository.saved == saved_with.args
def test_save_rejects_nonexistent_output_directory(qtbot, settings: AppSettings, tmp_path: Path) -> None:
repository = FakeRepository()
dialog = SettingsDialog(settings, repository, FakeCredentials())
qtbot.addWidget(dialog)
warnings: list[tuple[str, str]] = []
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr(
QMessageBox,
"warning",
lambda parent, title, text: warnings.append((title, text)),
)
try:
dialog.output_directory_input.setText(str(tmp_path / "missing"))
dialog.save()
finally:
monkeypatch.undo()
assert repository.saved == []
assert warnings == [("Invalid output directory", "Choose an accessible output directory.")]
assert dialog.isVisible() is False
def test_save_clears_key_field_and_never_writes_key_to_widgets(qtbot, settings: AppSettings) -> None:
dialog = SettingsDialog(settings, FakeRepository(), FakeCredentials())
qtbot.addWidget(dialog)
supplied_key = "test-token-for-dialog"
dialog.api_key_input.setText(supplied_key)
dialog.save()
assert dialog.api_key_input.text() == ""
assert all(supplied_key not in widget.text() for widget in dialog.findChildren(QLineEdit))
def test_service_failures_show_sanitized_message_and_keep_dialog_usable(
qtbot, settings: AppSettings, monkeypatch
) -> None:
class FailingCredentials(FakeCredentials):
def set_api_key(self, value: str) -> None:
raise CredentialError("backend detail must not be shown")
dialog = SettingsDialog(settings, FakeRepository(), FailingCredentials())
qtbot.addWidget(dialog)
warnings: list[tuple[str, str]] = []
monkeypatch.setattr(
QMessageBox,
"warning",
lambda parent, title, text: warnings.append((title, text)),
)
dialog.api_key_input.setText("test-token-for-dialog")
dialog.save()
assert warnings == [("Could not save settings", "The API key could not be saved.")]
assert dialog.result() == 0
assert dialog.api_key_input.text() == ""
def test_settings_repository_failure_is_sanitized(qtbot, settings: AppSettings, monkeypatch) -> None:
class FailingRepository(FakeRepository):
def save(self, settings: AppSettings) -> None:
raise SettingsError("filesystem detail must not be shown")
dialog = SettingsDialog(settings, FailingRepository(), FakeCredentials())
qtbot.addWidget(dialog)
warnings: list[tuple[str, str]] = []
monkeypatch.setattr(
QMessageBox,
"warning",
lambda parent, title, text: warnings.append((title, text)),
)
dialog.save()
assert warnings == [("Could not save settings", "Settings could not be saved.")]
assert dialog.result() == 0