feat: toggle all-certs view with A key (load_all_certs, Action.TOGGLE_ALL)
This commit is contained in:
@@ -124,6 +124,25 @@ def load_expiring_certs(
|
||||
return results
|
||||
|
||||
|
||||
def load_all_certs(pki_dir: str) -> list[CertInfo]:
|
||||
"""Return all valid (V-status) certs, sorted by expiry date."""
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
results: list[CertInfo] = []
|
||||
with open(os.path.join(pki_dir, "index.txt")) as fh:
|
||||
for line in fh:
|
||||
parsed = _parse_index_line(line)
|
||||
if parsed is None:
|
||||
continue
|
||||
cn, expiry = parsed
|
||||
results.append(CertInfo(
|
||||
cn=cn,
|
||||
expires=expiry,
|
||||
days_left=(expiry - now).days,
|
||||
))
|
||||
results.sort(key=lambda c: c.expires)
|
||||
return results
|
||||
|
||||
|
||||
# ============================================================
|
||||
# === PASSWORD ===
|
||||
# ============================================================
|
||||
@@ -609,6 +628,7 @@ class Action(Enum):
|
||||
NEW_CERT = auto()
|
||||
REGEN_CRL = auto()
|
||||
REVOKE = auto()
|
||||
TOGGLE_ALL = auto()
|
||||
EXIT = auto()
|
||||
|
||||
|
||||
@@ -632,7 +652,9 @@ def _expiry_label(c: CertInfo) -> str:
|
||||
return f"[exp in {c.days_left}d] "
|
||||
|
||||
|
||||
def show_main_screen(stdscr, certs: list[CertInfo]) -> ScreenResult:
|
||||
def show_main_screen(
|
||||
stdscr, certs: list[CertInfo], show_all: bool = False
|
||||
) -> ScreenResult:
|
||||
"""Display cert selection list. Returns when user activates an action."""
|
||||
init_colors()
|
||||
curses.curs_set(0)
|
||||
@@ -649,8 +671,9 @@ def show_main_screen(stdscr, certs: list[CertInfo]) -> ScreenResult:
|
||||
stdscr.clear()
|
||||
sh, sw = stdscr.getmaxyx()
|
||||
|
||||
# Header bar
|
||||
hdr = " openvpncertupdate — VPN Certificate Manager "
|
||||
# Header bar — shows current filter mode
|
||||
mode = "ALL certs" if show_all else "expiring/expired"
|
||||
hdr = f" openvpncertupdate — VPN Certificate Manager [{mode}] "
|
||||
try:
|
||||
stdscr.addstr(0, 0, hdr.ljust(sw)[:sw],
|
||||
curses.color_pair(COLOR_TITLE) | curses.A_BOLD)
|
||||
@@ -658,7 +681,7 @@ def show_main_screen(stdscr, certs: list[CertInfo]) -> ScreenResult:
|
||||
pass
|
||||
|
||||
# Footer hint
|
||||
hint = " ↑↓:Navigate Space:Check Enter:Confirm R:Revoke Q:Quit"
|
||||
hint = " ↑↓:Navigate Space:Check Enter:Confirm R:Revoke A:Toggle view Q:Quit"
|
||||
try:
|
||||
stdscr.addstr(sh - 1, 0, hint[:sw], curses.color_pair(COLOR_DISABLED))
|
||||
except curses.error:
|
||||
@@ -737,6 +760,9 @@ def show_main_screen(stdscr, certs: list[CertInfo]) -> ScreenResult:
|
||||
return ScreenResult(action=Action.EXIT)
|
||||
# _MENU_NEW when checked → grayed, ignore
|
||||
|
||||
elif key in (ord("a"), ord("A")):
|
||||
return ScreenResult(action=Action.TOGGLE_ALL)
|
||||
|
||||
elif key in (ord("r"), ord("R")) and is_cert(cursor):
|
||||
cert = certs[cursor]
|
||||
if show_confirm(
|
||||
@@ -759,19 +785,25 @@ class CursesApp:
|
||||
curses.curs_set(0)
|
||||
init_colors()
|
||||
|
||||
show_all = False
|
||||
|
||||
while True:
|
||||
try:
|
||||
certs = load_expiring_certs(
|
||||
EASYRSA_PKI_DIR, DAYS_PAST, DAYS_AHEAD,
|
||||
certs = (
|
||||
load_all_certs(EASYRSA_PKI_DIR)
|
||||
if show_all
|
||||
else load_expiring_certs(EASYRSA_PKI_DIR, DAYS_PAST, DAYS_AHEAD)
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
self._fatal(stdscr, f"Cannot read PKI index.txt:\n{exc}")
|
||||
return
|
||||
|
||||
result = show_main_screen(stdscr, certs)
|
||||
result = show_main_screen(stdscr, certs, show_all=show_all)
|
||||
|
||||
if result.action == Action.EXIT:
|
||||
return
|
||||
elif result.action == Action.TOGGLE_ALL:
|
||||
show_all = not show_all
|
||||
elif result.action == Action.REGEN_CRL:
|
||||
self._regen_crl(stdscr)
|
||||
elif result.action == Action.REVOKE:
|
||||
|
||||
@@ -282,3 +282,44 @@ def test_empty_cert_list_quit():
|
||||
keys = [ord("q")]
|
||||
result = _run(keys, certs=[])
|
||||
assert result.action == Action.EXIT
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# A / a → TOGGLE_ALL
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_a_lowercase_returns_toggle_all():
|
||||
result = _run([ord("a")])
|
||||
assert result.action == Action.TOGGLE_ALL
|
||||
|
||||
|
||||
def test_A_uppercase_returns_toggle_all():
|
||||
result = _run([ord("A")])
|
||||
assert result.action == Action.TOGGLE_ALL
|
||||
|
||||
|
||||
def test_toggle_all_with_no_certs():
|
||||
"""A works even when the cert list is empty."""
|
||||
result = _run([ord("a")], certs=[])
|
||||
assert result.action == Action.TOGGLE_ALL
|
||||
|
||||
|
||||
def test_show_all_flag_passed_through_to_header():
|
||||
"""show_all=True causes 'ALL certs' to appear in the header addstr call."""
|
||||
stdscr = _make_stdscr()
|
||||
stdscr.getch.side_effect = [ord("q")]
|
||||
with patch("openvpncertupdate.init_colors"):
|
||||
show_main_screen(stdscr, _CERTS, show_all=True)
|
||||
# The header addstr call (row 0) should contain 'ALL certs'
|
||||
calls = [str(c) for c in stdscr.addstr.call_args_list]
|
||||
assert any("ALL certs" in c for c in calls)
|
||||
|
||||
|
||||
def test_show_all_false_header_shows_expiring():
|
||||
"""show_all=False (default) shows 'expiring/expired' in the header."""
|
||||
stdscr = _make_stdscr()
|
||||
stdscr.getch.side_effect = [ord("q")]
|
||||
with patch("openvpncertupdate.init_colors"):
|
||||
show_main_screen(stdscr, _CERTS)
|
||||
calls = [str(c) for c in stdscr.addstr.call_args_list]
|
||||
assert any("expiring/expired" in c for c in calls)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import textwrap
|
||||
import pytest
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from openvpncertupdate import load_expiring_certs, CertInfo, _parse_index_line
|
||||
from openvpncertupdate import load_all_certs, load_expiring_certs, CertInfo, _parse_index_line
|
||||
|
||||
|
||||
def _fmt(dt: datetime) -> str:
|
||||
@@ -57,3 +57,22 @@ def test_parse_4digit_year():
|
||||
cn, dt = result
|
||||
assert cn == "future"
|
||||
assert dt.year == 2025
|
||||
|
||||
|
||||
def test_load_all_certs_includes_all_valid(tmp_path):
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
pki = make_pki(tmp_path, now)
|
||||
certs = load_all_certs(pki)
|
||||
cns = {c.cn for c in certs}
|
||||
# All V-status entries included regardless of expiry window
|
||||
assert cns == {"soon", "later", "past15", "past40"}
|
||||
# Revoked entry excluded
|
||||
assert "revoked" not in cns
|
||||
|
||||
|
||||
def test_load_all_certs_sorted_ascending(tmp_path):
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
pki = make_pki(tmp_path, now)
|
||||
certs = load_all_certs(pki)
|
||||
dates = [c.expires for c in certs]
|
||||
assert dates == sorted(dates)
|
||||
|
||||
Reference in New Issue
Block a user