"""Tests for the TUI re-issue workflow (CursesApp._process_cert). Uses the same mock-curses approach as test_dialogs.py — no real terminal needed. _msg()/_error() swallow curses.error, so a MagicMock stdscr is enough as long as getmaxyx() returns real ints. """ from unittest.mock import MagicMock, patch import curses as _curses # Stub curses constants/callables before importing the module under test. _curses.color_pair = lambda x: 0 _curses.A_BOLD = 0 _curses.A_UNDERLINE = 0 _curses.curs_set = lambda x: None from openvpncertupdate import CursesApp, CertFormResult def _stdscr(): s = MagicMock() s.getmaxyx.return_value = (24, 80) # `sh - 2` needs a real int s.getch.return_value = ord("q") return s def _patch_workflow(monkeypatch, cert_file_present): """Patch everything _process_cert touches after the form is confirmed.""" monkeypatch.setattr( "openvpncertupdate.show_cert_form", MagicMock(return_value=CertFormResult( cn="y.kuts", email="", password="Testpass1234567890abcdefgh", confirmed=True))) monkeypatch.setattr("openvpncertupdate.has_issued_cert", MagicMock(return_value=cert_file_present)) for name, retval in ( ("revoke_issued", None), ("gen_crl", None), ("copy_crl", None), ("build_client_full", None), ("build_ovpn", "/out/y.kuts_2026-08-15_01/client.ovpn"), ("create_note", "https://cg.example.com/note/abc#deadbeef"), ): monkeypatch.setattr(f"openvpncertupdate.{name}", MagicMock(return_value=retval)) import openvpncertupdate as m return {n: getattr(m, n) for n in ( "revoke_issued", "gen_crl", "copy_crl", "build_client_full")} def test_tui_reissue_skips_revoke_when_cert_file_missing(monkeypatch): # The bug as reported: the TUI offered renewal for a CN whose .crt was # never carried over from the older EasyRSA install, and revoke-issued # failed. There is nothing to revoke — build the replacement instead. mocks = _patch_workflow(monkeypatch, cert_file_present=False) app = CursesApp() assert app._process_cert(_stdscr(), "y.kuts", "", is_renewal=True) is True mocks["revoke_issued"].assert_not_called() mocks["gen_crl"].assert_not_called() mocks["copy_crl"].assert_not_called() mocks["build_client_full"].assert_called_once() assert mocks["build_client_full"].call_args.args[2] == "y.kuts" def test_tui_reissue_logs_the_skip(monkeypatch): _patch_workflow(monkeypatch, cert_file_present=False) app = CursesApp() app._process_cert(_stdscr(), "y.kuts", "", is_renewal=True) assert any("revoke skipped" in e for e in app._session_log) def test_tui_reissue_revokes_when_cert_file_present(monkeypatch): mocks = _patch_workflow(monkeypatch, cert_file_present=True) app = CursesApp() assert app._process_cert(_stdscr(), "y.kuts", "", is_renewal=True) is True mocks["revoke_issued"].assert_called_once() mocks["gen_crl"].assert_called_once() mocks["copy_crl"].assert_called_once() mocks["build_client_full"].assert_called_once() def test_tui_skipped_revoke_build_failure_does_not_claim_revocation(monkeypatch): # Nothing was revoked, so the "has been revoked" warning would be a lie. import openvpncertupdate as m _patch_workflow(monkeypatch, cert_file_present=False) monkeypatch.setattr("openvpncertupdate.build_client_full", MagicMock(side_effect=m.EasyRSAError("boom"))) app = CursesApp() shown = [] monkeypatch.setattr(CursesApp, "_error", lambda self, stdscr, text: shown.append(text)) app._process_cert(_stdscr(), "y.kuts", "", is_renewal=True) assert shown and "has been revoked" not in shown[0]