Reissue internally revokes the old cert (revoke_issued), which makes the published CRL immediately stale, but neither the CLI --reissue path nor the TUI renew flow ever regenerated/copied it — only standalone revoke and "Regenerate CRL" did. A cert revoked as part of a reissue could keep authenticating against OpenVPN until someone remembered to regenerate the CRL by hand. Both reissue paths now call gen_crl()+copy_crl() right after a successful revoke_issued(), same as standalone revoke. This isn't gated on the subsequent build_client_full() succeeding, since the CRL is already stale the moment the old cert is revoked. A CRL-regen failure here is reported as a warning and the reissue continues to build the replacement cert — a new cert is more urgent than a fresh CRL, and "Regenerate CRL"/--gen-crl remain available to retry. Also add a RESTORECON_BINARY setting (default "restorecon", "" to disable): copy_crl() now runs it on the destination file after every copy, everywhere copy_crl() is called. Best-effort like is_ca_key_encrypted() — a missing/misconfigured binary is swallowed rather than breaking CRL deployment on non-SELinux systems. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
525 lines
21 KiB
Python
525 lines
21 KiB
Python
"""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.gen_crl", MagicMock())
|
|
monkeypatch.setattr("openvpncertupdate.copy_crl", MagicMock())
|
|
monkeypatch.setattr("openvpncertupdate.build_client_full", 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,
|
|
"gen_crl": sys.modules["openvpncertupdate"].gen_crl,
|
|
"copy_crl": sys.modules["openvpncertupdate"].copy_crl,
|
|
"build_client_full": sys.modules["openvpncertupdate"].build_client_full,
|
|
"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_passes_email_to_build_client_full(monkeypatch, capsys):
|
|
# build-client-full embeds email into the cert subject (EASYRSA_REQ_EMAIL),
|
|
# which is what index.txt / get_email() reads back later — there's no
|
|
# separate email store to save to.
|
|
mocks = _patch_issue(monkeypatch)
|
|
CliRunner().create("alice", "alice@example.com")
|
|
assert mocks["build_client_full"].call_args.kwargs["email"] == "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_regenerates_and_copies_crl(monkeypatch, capsys):
|
|
# The old cert is revoked as part of reissue, so the published CRL is
|
|
# stale until regenerated — reissue must do that itself now.
|
|
mocks = _patch_issue(monkeypatch)
|
|
CliRunner().reissue("bob", "bob@example.com")
|
|
mocks["gen_crl"].assert_called_once()
|
|
mocks["copy_crl"].assert_called_once()
|
|
|
|
|
|
def test_create_does_not_touch_crl(monkeypatch, capsys):
|
|
# --create never revokes anything, so there's nothing stale to fix.
|
|
mocks = _patch_issue(monkeypatch)
|
|
CliRunner().create("alice", "alice@example.com")
|
|
mocks["gen_crl"].assert_not_called()
|
|
mocks["copy_crl"].assert_not_called()
|
|
|
|
|
|
def test_reissue_continues_building_when_crl_regen_fails(monkeypatch, capsys):
|
|
import openvpncertupdate
|
|
mocks = _patch_issue(monkeypatch)
|
|
monkeypatch.setattr("openvpncertupdate.gen_crl",
|
|
MagicMock(side_effect=openvpncertupdate.EasyRSAError("gen-crl failed")))
|
|
CliRunner().reissue("bob", "bob@example.com")
|
|
mocks["build_client_full"].assert_called_once()
|
|
assert "CRL update failed" in capsys.readouterr().err
|
|
|
|
|
|
def test_reissue_falls_back_to_index_txt_email(monkeypatch, capsys):
|
|
# get_email() reads straight from index.txt; this stubs that lookup
|
|
# rather than the file itself (index.txt parsing is covered in test_pki.py).
|
|
mocks = _patch_issue(monkeypatch)
|
|
monkeypatch.setattr("openvpncertupdate.get_email",
|
|
MagicMock(return_value="fromindex@example.com"))
|
|
CliRunner().reissue("bob", "") # no --email supplied
|
|
mocks["send_email"].assert_called_once()
|
|
assert mocks["send_email"].call_args.args[0] == "fromindex@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, err = capsys.readouterr()
|
|
assert "config:" in out
|
|
assert "warning: email skipped (no email address on file for bob)" in err
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _format_cert_table
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_format_cert_table_empty_list():
|
|
import openvpncertupdate
|
|
assert openvpncertupdate._format_cert_table([]) == "(no certificates)"
|
|
|
|
|
|
def test_format_cert_table_header_and_columns(monkeypatch):
|
|
import openvpncertupdate
|
|
certs = [
|
|
openvpncertupdate.CertInfo(cn="alice", expires=None, days_left=24),
|
|
openvpncertupdate.CertInfo(cn="bob", expires=None, days_left=-5),
|
|
]
|
|
monkeypatch.setattr(
|
|
"openvpncertupdate.get_email",
|
|
MagicMock(side_effect=lambda pki, cn: {"alice": "alice@example.com"}.get(cn, "")),
|
|
)
|
|
table = openvpncertupdate._format_cert_table(certs)
|
|
lines = table.splitlines()
|
|
assert lines[0].split() == ["CN", "EXPIRES", "EMAIL"]
|
|
assert len(lines) == 3
|
|
|
|
|
|
def test_format_cert_table_shows_email_and_none_placeholder(monkeypatch):
|
|
import openvpncertupdate
|
|
certs = [
|
|
openvpncertupdate.CertInfo(cn="alice", expires=None, days_left=24),
|
|
openvpncertupdate.CertInfo(cn="bob", expires=None, days_left=-5),
|
|
]
|
|
monkeypatch.setattr(
|
|
"openvpncertupdate.get_email",
|
|
MagicMock(side_effect=lambda pki, cn: {"alice": "alice@example.com"}.get(cn, "")),
|
|
)
|
|
table = openvpncertupdate._format_cert_table(certs)
|
|
lines = table.splitlines()
|
|
assert "alice@example.com" in lines[1]
|
|
assert "(none)" in lines[2]
|
|
|
|
|
|
def test_format_cert_table_columns_are_aligned(monkeypatch):
|
|
import openvpncertupdate
|
|
certs = [
|
|
openvpncertupdate.CertInfo(cn="a", expires=None, days_left=24),
|
|
openvpncertupdate.CertInfo(cn="a-much-longer", expires=None, days_left=5),
|
|
]
|
|
monkeypatch.setattr("openvpncertupdate.get_email", MagicMock(return_value=""))
|
|
table = openvpncertupdate._format_cert_table(certs)
|
|
lines = table.splitlines()
|
|
# The EMAIL column must start at the same character offset on every row.
|
|
email_col = len(lines[0]) - len("EMAIL")
|
|
for line in lines[1:]:
|
|
assert line.rstrip().endswith("(none)")
|
|
assert line.index("(none)") == email_col
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CliRunner.list_certs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_list_certs_expiring_uses_days_settings(monkeypatch, capsys):
|
|
mock_load = MagicMock(return_value=[])
|
|
monkeypatch.setattr("openvpncertupdate.load_expiring_certs", mock_load)
|
|
monkeypatch.setattr("openvpncertupdate.DAYS_PAST", 30)
|
|
monkeypatch.setattr("openvpncertupdate.DAYS_AHEAD", 14)
|
|
monkeypatch.setattr("openvpncertupdate.EASYRSA_PKI_DIR", "/pki")
|
|
|
|
CliRunner().list_certs(show_all=False)
|
|
|
|
mock_load.assert_called_once_with("/pki", 30, 14)
|
|
assert "(no certificates)" in capsys.readouterr().out
|
|
|
|
|
|
def test_list_certs_all_uses_load_all_certs(monkeypatch, capsys):
|
|
mock_load = MagicMock(return_value=[])
|
|
monkeypatch.setattr("openvpncertupdate.load_all_certs", mock_load)
|
|
monkeypatch.setattr("openvpncertupdate.EASYRSA_PKI_DIR", "/pki")
|
|
|
|
CliRunner().list_certs(show_all=True)
|
|
|
|
mock_load.assert_called_once_with("/pki")
|
|
assert "(no certificates)" in capsys.readouterr().out
|
|
|
|
|
|
def test_list_certs_prints_formatted_table(monkeypatch, capsys):
|
|
import openvpncertupdate
|
|
certs = [openvpncertupdate.CertInfo(cn="alice", expires=None, days_left=10)]
|
|
monkeypatch.setattr("openvpncertupdate.load_expiring_certs", MagicMock(return_value=certs))
|
|
monkeypatch.setattr("openvpncertupdate.get_email", MagicMock(return_value="alice@example.com"))
|
|
|
|
CliRunner().list_certs(show_all=False)
|
|
|
|
out = capsys.readouterr().out
|
|
assert "alice" in out
|
|
assert "alice@example.com" in 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, show_eml=False)
|
|
|
|
|
|
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, show_eml=False)
|
|
|
|
|
|
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_dispatches_list(monkeypatch):
|
|
monkeypatch.setattr(sys, "argv", ["prog", "--list"])
|
|
runner = MagicMock()
|
|
monkeypatch.setattr("openvpncertupdate.CliRunner", lambda: runner)
|
|
main()
|
|
runner.list_certs.assert_called_once_with(show_all=False)
|
|
|
|
|
|
def test_main_dispatches_list_all(monkeypatch):
|
|
monkeypatch.setattr(sys, "argv", ["prog", "--list-all"])
|
|
runner = MagicMock()
|
|
monkeypatch.setattr("openvpncertupdate.CliRunner", lambda: runner)
|
|
main()
|
|
runner.list_certs.assert_called_once_with(show_all=True)
|
|
|
|
|
|
def test_main_list_skips_ca_passphrase_resolution(monkeypatch):
|
|
monkeypatch.setattr(sys, "argv", ["prog", "--list"])
|
|
monkeypatch.setattr("openvpncertupdate.CliRunner", lambda: MagicMock())
|
|
mock_resolve = MagicMock()
|
|
monkeypatch.setattr("openvpncertupdate.resolve_ca_passphrase", mock_resolve)
|
|
main()
|
|
mock_resolve.assert_not_called()
|
|
|
|
|
|
def test_main_list_all_skips_ca_passphrase_resolution(monkeypatch):
|
|
monkeypatch.setattr(sys, "argv", ["prog", "--list-all"])
|
|
monkeypatch.setattr("openvpncertupdate.CliRunner", lambda: MagicMock())
|
|
mock_resolve = MagicMock()
|
|
monkeypatch.setattr("openvpncertupdate.resolve_ca_passphrase", mock_resolve)
|
|
main()
|
|
mock_resolve.assert_not_called()
|
|
|
|
|
|
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()
|
|
|
|
|
|
def test_main_resolves_ca_passphrase_before_dispatch(monkeypatch, tmp_path):
|
|
"""The configured CA_PASSPHRASE sentinel must be resolved to an actual
|
|
passphrase before any EasyRSA call, and that resolved value (not the
|
|
sentinel) must be what reaches them."""
|
|
monkeypatch.setattr(sys, "argv", ["prog", "--gen-crl"])
|
|
monkeypatch.setattr("openvpncertupdate.CA_PASSPHRASE", "!ask")
|
|
monkeypatch.setattr("openvpncertupdate.EASYRSA_DIR", "/er")
|
|
monkeypatch.setattr("openvpncertupdate.EASYRSA_PKI_DIR", str(tmp_path))
|
|
mock_gen_crl = MagicMock()
|
|
monkeypatch.setattr("openvpncertupdate.gen_crl", mock_gen_crl)
|
|
monkeypatch.setattr("openvpncertupdate.copy_crl", MagicMock())
|
|
mock_resolve = MagicMock(return_value="typed-pass")
|
|
monkeypatch.setattr("openvpncertupdate.resolve_ca_passphrase", mock_resolve)
|
|
|
|
main()
|
|
|
|
mock_resolve.assert_called_once_with("!ask", str(tmp_path))
|
|
|
|
|
|
def test_main_applies_config_overrides_before_dispatch(monkeypatch, tmp_path):
|
|
"""Overrides returned by load_settings_overrides() must land in the module
|
|
globals before EasyRSA calls, so a config file can redirect EASYRSA_PKI_DIR etc."""
|
|
monkeypatch.setattr(sys, "argv", ["prog", "--gen-crl"])
|
|
monkeypatch.setattr("openvpncertupdate.EASYRSA_DIR", "/er")
|
|
monkeypatch.setattr("openvpncertupdate.EASYRSA_PKI_DIR", "/original/pki")
|
|
monkeypatch.setattr("openvpncertupdate.CA_PASSPHRASE", "!empty")
|
|
mock_gen_crl = MagicMock()
|
|
monkeypatch.setattr("openvpncertupdate.gen_crl", mock_gen_crl)
|
|
monkeypatch.setattr("openvpncertupdate.copy_crl", MagicMock())
|
|
mock_load = MagicMock(return_value={"EASYRSA_PKI_DIR": "/overridden/pki"})
|
|
monkeypatch.setattr("openvpncertupdate.load_settings_overrides", mock_load)
|
|
|
|
main()
|
|
|
|
mock_gen_crl.assert_called_once_with("/er", "/overridden/pki", "")
|
|
|
|
|
|
def test_main_passes_config_flag_to_load_overrides(monkeypatch):
|
|
monkeypatch.setattr(sys, "argv", ["prog", "--gen-crl", "--config", "/explicit/path.conf"])
|
|
monkeypatch.setattr("openvpncertupdate.CA_PASSPHRASE", "!empty")
|
|
monkeypatch.setattr("openvpncertupdate.CliRunner", lambda: MagicMock())
|
|
mock_load = MagicMock(return_value={})
|
|
monkeypatch.setattr("openvpncertupdate.load_settings_overrides", mock_load)
|
|
|
|
main()
|
|
|
|
assert mock_load.call_args[0][0] == "/explicit/path.conf"
|
|
|
|
|
|
def test_main_exits_with_error_when_explicit_config_missing(monkeypatch, capsys):
|
|
monkeypatch.setattr(sys, "argv", ["prog", "--gen-crl", "--config", "/does/not/exist.conf"])
|
|
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
main()
|
|
|
|
assert exc_info.value.code != 0
|
|
assert "/does/not/exist.conf" in capsys.readouterr().err
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# --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, show_eml=False)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# --show-eml
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_show_eml_outputs_base64(monkeypatch, capsys, tmp_path):
|
|
"""--show-eml prints a non-empty base64 string to stdout."""
|
|
import base64 as _b64
|
|
# Write a minimal template and .ovpn so build_mime_message can read them
|
|
template = tmp_path / "tpl.txt"
|
|
template.write_text("Hi {cn}, url={url}, cfg={config_name}")
|
|
ovpn = tmp_path / "c.ovpn"
|
|
ovpn.write_bytes(b"ovpn-data")
|
|
|
|
mocks = _patch_issue(monkeypatch, ovpn_path=str(ovpn))
|
|
import openvpncertupdate as m
|
|
monkeypatch.setattr(m, "EMAIL_TEMPLATE_PATH", str(template))
|
|
monkeypatch.setattr(m, "build_mime_message",
|
|
m.build_mime_message) # keep real implementation
|
|
|
|
CliRunner().create("alice", "alice@example.com",
|
|
send_email_flag=False, show_eml=True)
|
|
out = capsys.readouterr().out
|
|
# Find the base64 block (before the "config:" line)
|
|
b64_line = [l for l in out.splitlines() if l and not l.startswith(("config:", "password"))][0]
|
|
decoded = _b64.b64decode(b64_line)
|
|
assert b"alice@example.com" in decoded
|
|
assert b"alice" in decoded
|
|
|
|
|
|
def test_show_eml_implies_no_send(monkeypatch, capsys):
|
|
"""--show-eml without --send-email must not call send_email."""
|
|
mocks = _patch_issue(monkeypatch)
|
|
import openvpncertupdate as m
|
|
monkeypatch.setattr(m, "build_mime_message", MagicMock(
|
|
return_value=MagicMock(as_bytes=MagicMock(return_value=b"eml"))
|
|
))
|
|
CliRunner().create("alice", "alice@example.com",
|
|
send_email_flag=False, show_eml=True)
|
|
mocks["send_email"].assert_not_called()
|
|
|
|
|
|
def test_show_eml_with_send_email_still_sends(monkeypatch, capsys):
|
|
"""--show-eml --send-email: both show eml and send."""
|
|
mocks = _patch_issue(monkeypatch)
|
|
import openvpncertupdate as m
|
|
monkeypatch.setattr(m, "build_mime_message", MagicMock(
|
|
return_value=MagicMock(as_bytes=MagicMock(return_value=b"eml"))
|
|
))
|
|
CliRunner().create("alice", "alice@example.com",
|
|
send_email_flag=True, show_eml=True)
|
|
mocks["send_email"].assert_called_once()
|
|
|
|
|
|
def test_main_show_eml_defaults_to_no_send(monkeypatch):
|
|
"""--show-eml with no explicit --send-email → send_email_flag=False."""
|
|
monkeypatch.setattr(sys, "argv",
|
|
["prog", "--create", "alice", "--email", "a@b.com", "--show-eml"])
|
|
runner = MagicMock()
|
|
monkeypatch.setattr("openvpncertupdate.CliRunner", lambda: runner)
|
|
main()
|
|
runner.create.assert_called_once_with(
|
|
"alice", "a@b.com", send_email_flag=False, show_eml=True
|
|
)
|
|
|
|
|
|
def test_main_show_eml_send_email_override(monkeypatch):
|
|
"""--show-eml --send-email → send_email_flag=True."""
|
|
monkeypatch.setattr(sys, "argv",
|
|
["prog", "--create", "alice", "--email", "a@b.com",
|
|
"--show-eml", "--send-email"])
|
|
runner = MagicMock()
|
|
monkeypatch.setattr("openvpncertupdate.CliRunner", lambda: runner)
|
|
main()
|
|
runner.create.assert_called_once_with(
|
|
"alice", "a@b.com", send_email_flag=True, show_eml=True
|
|
)
|