From 444e35f8cd588ee0e9e63bba02aadd606d6afe22 Mon Sep 17 00:00:00 2001 From: Vlad Doloman Date: Wed, 8 Jul 2026 16:20:41 +0300 Subject: [PATCH] add _format_cert_table() helper for CLI cert listings Co-Authored-By: Claude Sonnet 5 --- openvpncertupdate.py | 16 +++++++++++++ tests/test_cli.py | 57 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/openvpncertupdate.py b/openvpncertupdate.py index 248cba8..998c899 100644 --- a/openvpncertupdate.py +++ b/openvpncertupdate.py @@ -1262,6 +1262,22 @@ class CursesApp: # ============================================================ +def _format_cert_table(certs: list[CertInfo]) -> str: + """Render CN / expiry / email as an aligned table, like `ls -l`.""" + if not certs: + return "(no certificates)" + rows = [ + (c.cn, _expiry_label(c).strip(), get_email(EASYRSA_PKI_DIR, c.cn) or "(none)") + for c in certs + ] + cn_w = max(len("CN"), max(len(r[0]) for r in rows)) + exp_w = max(len("EXPIRES"), max(len(r[1]) for r in rows)) + lines = [f"{'CN':<{cn_w}} {'EXPIRES':<{exp_w}} EMAIL"] + for cn, exp, email in rows: + lines.append(f"{cn:<{cn_w}} {exp:<{exp_w}} {email}") + return "\n".join(lines) + + class CliRunner: """Non-interactive runner for scripting / cron use.""" diff --git a/tests/test_cli.py b/tests/test_cli.py index 1a8eaac..0ad436a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -150,6 +150,63 @@ def test_regen_crl_calls_gen_and_copy(monkeypatch, capsys): 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 + + # --------------------------------------------------------------------------- # main() argument dispatch # ---------------------------------------------------------------------------