Guard --reissue against unknown CNs and leftover build files

Code review findings on the "skip revoke when .crt is missing" migration
path:

- CliRunner._issue() took the skip path whenever has_issued_cert() was
  False, which is also true for a typo'd/nonexistent CN — it would warn,
  skip the revoke, and go on to build, package, and email a brand-new
  certificate for a CN nobody asked to renew. The skip now only fires when
  the CN has a current index.txt entry (via _load_current_certs()); an
  unknown CN prints an error and exits 1 with nothing built. The TUI's
  _process_cert() doesn't need the same guard — renewal there always opens
  on an existing row (cn_readonly pins the CN), so a typo'd CN can't reach
  the branch.

- Skipping the revoke leaves pki/reqs/<CN>.req and pki/private/<CN>.key in
  place (normally revoke-issued archives both), which makes EasyRSA's
  build-client-full abort. Both CliRunner._issue() and
  CursesApp._process_cert() now check for those leftovers before building
  and fail fast with the exact paths, rather than surfacing EasyRSA's
  confusing error after the CA passphrase prompt. Neither path touches the
  files itself.

Also: strengthened two under-specified tests (test_main_rejects_bad_days_flag
now checks the resolver's message text, not just "--days", which also
appears in argparse's unrelated error; test_show_cert_form_confirm now pins
the Enter-keypress count so a partial "days" field reversion is caught), and
folded a malformed CLAUDE.md table row into its neighbor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Vlad Doloman
2026-08-15 05:58:32 +03:00
parent 68f481095a
commit 1d059db083
5 changed files with 209 additions and 8 deletions

View File

@@ -100,6 +100,43 @@ def test_tui_seeds_days_field_from_cert_days(monkeypatch):
assert m.show_cert_form.call_args.kwargs["days"] == "90"
def test_tui_reissue_blocks_skip_when_leftover_req_exists(monkeypatch, tmp_path):
# revoke-issued normally archives pki/reqs/<CN>.req and
# pki/private/<CN>.key into pki/revoked/; when it's skipped (no issued
# .crt to revoke) those leftovers make build-client-full abort. Catch it
# with an error dialog instead of letting the confusing EasyRSA error
# surface after the CA passphrase prompt.
(tmp_path / "reqs").mkdir()
req_path = tmp_path / "reqs" / "y.kuts.req"
req_path.write_text("leftover request")
monkeypatch.setattr("openvpncertupdate.EASYRSA_PKI_DIR", str(tmp_path))
mocks = _patch_workflow(monkeypatch, cert_file_present=False)
app = CursesApp()
shown = []
monkeypatch.setattr(CursesApp, "_error",
lambda self, stdscr, text: shown.append(text))
result = app._process_cert(_stdscr(), "y.kuts", "", is_renewal=True)
assert result is True
assert shown and str(req_path) in shown[0]
mocks["build_client_full"].assert_not_called()
def test_tui_reissue_blocks_skip_when_leftover_key_exists(monkeypatch, tmp_path):
(tmp_path / "private").mkdir()
key_path = tmp_path / "private" / "y.kuts.key"
key_path.write_text("leftover key")
monkeypatch.setattr("openvpncertupdate.EASYRSA_PKI_DIR", str(tmp_path))
mocks = _patch_workflow(monkeypatch, cert_file_present=False)
app = CursesApp()
shown = []
monkeypatch.setattr(CursesApp, "_error",
lambda self, stdscr, text: shown.append(text))
result = app._process_cert(_stdscr(), "y.kuts", "", is_renewal=True)
assert result is True
assert shown and str(key_path) in shown[0]
mocks["build_client_full"].assert_not_called()
def test_tui_forwards_form_days_to_build_client_full(monkeypatch):
mocks = _patch_workflow(monkeypatch, cert_file_present=True)
monkeypatch.setattr(