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
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
|
|
|