"""Tests for CLI mode (CliRunner + main() argument dispatch).""" import sys from unittest.mock import MagicMock, patch, call import pytest from openvpncertupdate import CliRunner, _build_parser, main # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _patch_issue(monkeypatch, ovpn_path="/out/cn_2026-01-01/client.ovpn", one_time_url="https://cg.example.com/#/note/abc/xyz"): """Patch all side-effectful callables used by CliRunner._issue.""" monkeypatch.setattr("openvpncertupdate.revoke_issued", MagicMock()) monkeypatch.setattr("openvpncertupdate.build_client_full", MagicMock()) monkeypatch.setattr("openvpncertupdate.save_email", MagicMock()) monkeypatch.setattr("openvpncertupdate.build_ovpn", MagicMock(return_value=ovpn_path)) monkeypatch.setattr("openvpncertupdate.create_note", MagicMock(return_value=one_time_url)) monkeypatch.setattr("openvpncertupdate.send_email", MagicMock()) monkeypatch.setattr("openvpncertupdate.generate_password", MagicMock(return_value="Testpass1234567890abcdefgh")) return { "revoke_issued": sys.modules["openvpncertupdate"].revoke_issued, "build_client_full": sys.modules["openvpncertupdate"].build_client_full, "save_email": sys.modules["openvpncertupdate"].save_email, "build_ovpn": sys.modules["openvpncertupdate"].build_ovpn, "create_note": sys.modules["openvpncertupdate"].create_note, "send_email": sys.modules["openvpncertupdate"].send_email, } # --------------------------------------------------------------------------- # CliRunner.create # --------------------------------------------------------------------------- def test_create_calls_build_not_revoke(monkeypatch, capsys): mocks = _patch_issue(monkeypatch) CliRunner().create("alice", "alice@example.com") mocks["revoke_issued"].assert_not_called() mocks["build_client_full"].assert_called_once() _, kwargs = mocks["build_client_full"].call_args # cn is the third positional arg assert mocks["build_client_full"].call_args.args[2] == "alice" def test_create_saves_email(monkeypatch, capsys): mocks = _patch_issue(monkeypatch) CliRunner().create("alice", "alice@example.com") mocks["save_email"].assert_called_once() assert mocks["save_email"].call_args.args[1] == "alice" assert mocks["save_email"].call_args.args[2] == "alice@example.com" def test_create_sends_email_and_prints_url(monkeypatch, capsys): url = "https://cg.example.com/#/note/abc/xyz" mocks = _patch_issue(monkeypatch, one_time_url=url) CliRunner().create("alice", "alice@example.com") mocks["send_email"].assert_called_once() out = capsys.readouterr().out assert "password-url:" in out assert url in out assert "password:" not in out def test_create_prints_password_when_cryptgeon_fails(monkeypatch, capsys): _patch_issue(monkeypatch) import openvpncertupdate monkeypatch.setattr("openvpncertupdate.create_note", MagicMock(side_effect=openvpncertupdate.CryptgeonError("down"))) CliRunner().create("alice", "alice@example.com") out = capsys.readouterr().out assert "password:" in out assert "password-url:" not in out # --------------------------------------------------------------------------- # CliRunner.reissue # --------------------------------------------------------------------------- def test_reissue_calls_revoke_then_build(monkeypatch, capsys): mocks = _patch_issue(monkeypatch) CliRunner().reissue("bob", "bob@example.com") mocks["revoke_issued"].assert_called_once() mocks["build_client_full"].assert_called_once() # revoke must happen before build assert mocks["revoke_issued"].call_args.args[2] == "bob" assert mocks["build_client_full"].call_args.args[2] == "bob" def test_reissue_falls_back_to_stored_email(monkeypatch, capsys): mocks = _patch_issue(monkeypatch) monkeypatch.setattr("openvpncertupdate.get_email", MagicMock(return_value="stored@example.com")) CliRunner().reissue("bob", "") # no --email supplied mocks["send_email"].assert_called_once() assert mocks["send_email"].call_args.args[0] == "stored@example.com" def test_reissue_no_email_no_send(monkeypatch, capsys): mocks = _patch_issue(monkeypatch) monkeypatch.setattr("openvpncertupdate.get_email", MagicMock(return_value="")) CliRunner().reissue("bob", "") mocks["send_email"].assert_not_called() out = capsys.readouterr().out assert "config:" in out # --------------------------------------------------------------------------- # CliRunner.revoke # --------------------------------------------------------------------------- def test_revoke_calls_revoke_gen_crl_copy_crl(monkeypatch, capsys): rev = MagicMock() gcrl = MagicMock() ccrl = MagicMock() monkeypatch.setattr("openvpncertupdate.revoke_issued", rev) monkeypatch.setattr("openvpncertupdate.gen_crl", gcrl) monkeypatch.setattr("openvpncertupdate.copy_crl", ccrl) CliRunner().revoke("charlie") rev.assert_called_once() gcrl.assert_called_once() ccrl.assert_called_once() assert "charlie" in capsys.readouterr().out def test_revoke_exits_on_error(monkeypatch): import openvpncertupdate monkeypatch.setattr("openvpncertupdate.revoke_issued", MagicMock(side_effect=openvpncertupdate.EasyRSAError("fail"))) with pytest.raises(SystemExit) as exc_info: CliRunner().revoke("charlie") assert exc_info.value.code == 1 # --------------------------------------------------------------------------- # CliRunner.regen_crl # --------------------------------------------------------------------------- def test_regen_crl_calls_gen_and_copy(monkeypatch, capsys): gcrl = MagicMock() ccrl = MagicMock() monkeypatch.setattr("openvpncertupdate.gen_crl", gcrl) monkeypatch.setattr("openvpncertupdate.copy_crl", ccrl) CliRunner().regen_crl() gcrl.assert_called_once() ccrl.assert_called_once() assert "CRL" in capsys.readouterr().out # --------------------------------------------------------------------------- # main() argument dispatch # --------------------------------------------------------------------------- def test_main_create_requires_email(monkeypatch): monkeypatch.setattr(sys, "argv", ["prog", "--create", "alice"]) with pytest.raises(SystemExit) as exc_info: main() assert exc_info.value.code != 0 def test_main_dispatches_create(monkeypatch): monkeypatch.setattr(sys, "argv", ["prog", "--create", "alice", "--email", "a@b.com"]) runner = MagicMock() monkeypatch.setattr("openvpncertupdate.CliRunner", lambda: runner) main() runner.create.assert_called_once_with("alice", "a@b.com", send_email_flag=True) def test_main_dispatches_reissue(monkeypatch): monkeypatch.setattr(sys, "argv", ["prog", "--reissue", "bob"]) runner = MagicMock() monkeypatch.setattr("openvpncertupdate.CliRunner", lambda: runner) main() runner.reissue.assert_called_once_with("bob", "", send_email_flag=True) def test_main_dispatches_revoke(monkeypatch): monkeypatch.setattr(sys, "argv", ["prog", "--revoke", "charlie"]) runner = MagicMock() monkeypatch.setattr("openvpncertupdate.CliRunner", lambda: runner) main() runner.revoke.assert_called_once_with("charlie") def test_main_dispatches_gen_crl(monkeypatch): monkeypatch.setattr(sys, "argv", ["prog", "--gen-crl"]) runner = MagicMock() monkeypatch.setattr("openvpncertupdate.CliRunner", lambda: runner) main() runner.regen_crl.assert_called_once_with() def test_main_no_args_launches_tui(monkeypatch): monkeypatch.setattr(sys, "argv", ["prog"]) app = MagicMock() monkeypatch.setattr("openvpncertupdate.CursesApp", lambda: app) main() app.run.assert_called_once_with() # --------------------------------------------------------------------------- # --send-email / --no-send-email # --------------------------------------------------------------------------- def test_no_send_email_skips_delivery(monkeypatch, capsys): mocks = _patch_issue(monkeypatch) monkeypatch.setattr("openvpncertupdate.get_email", MagicMock(return_value="")) CliRunner().create("alice", "alice@example.com", send_email_flag=False) mocks["send_email"].assert_not_called() out = capsys.readouterr().out assert "config:" in out def test_main_no_send_email_flag(monkeypatch): monkeypatch.setattr(sys, "argv", ["prog", "--reissue", "bob", "--no-send-email"]) runner = MagicMock() monkeypatch.setattr("openvpncertupdate.CliRunner", lambda: runner) main() runner.reissue.assert_called_once_with("bob", "", send_email_flag=False)