Skip to content

Commit ce1d0fd

Browse files
committed
fix: harmonise API, types, reporter, and test coverage
- Remove probe_raw from package re-exports (__init__.py); callers import from quantumvalidator.tls_utils directly - Correct timeout type int→float in _run_openssl, _read_server_banner, _probe_ssh, _probe_ftp; remove int() cast in cli.py - Replace falsy `if starttls:` with `if starttls is not None:` in _build_cmd (mirrors probe_raw fix) - Add _VALID_STARTTLS frozenset; probe_raw now raises ValueError for unrecognised STARTTLS modes - Fix reporter.save_report() to write from _console, not public alias; rename con→console in private helpers - Move import logging as _log to module level in constants.py; fix import order in __init__.py (PEP 8) - Add --cov-fail-under=100 to pyproject.toml addopts - Fix test_exits_2_on_missing_target, test_exits_2_on_save_error; add TestInfoGroups (5 tests); fix _capture helper in test_reporter.py - Update CLAUDE.md: openssl ≥ 3.5, --notes-file release pattern, reporter convention, main branch URLs throughout
1 parent 6558003 commit ce1d0fd

12 files changed

Lines changed: 230 additions & 147 deletions

File tree

CHANGELOG.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,61 @@ Version numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html
99

1010
## [Unreleased]
1111

12+
### Removed
13+
- `__init__.py`: `probe_raw` removed from package re-exports — callers (e.g.
14+
vendored copy in `mailvalidator`) import directly from `quantumvalidator.tls_utils`.
15+
16+
### Fixed
17+
- `tls_utils.probe_raw`: validate `starttls` parameter against the known set of
18+
openssl STARTTLS modes; raises `ValueError` for unrecognised values instead of
19+
forwarding arbitrary strings to the subprocess (defence-in-depth).
20+
- `tls_utils.probe_raw`: docstring now enumerates all four `None`-return
21+
conditions (openssl missing, invalid host/port, timeout, OSError) and documents
22+
the `ValueError` raised for invalid `starttls`.
23+
- `tls_utils.probe_raw`: `if starttls:` guard replaced with `if starttls is not None:`
24+
so a pre-validated mode is passed consistently.
25+
- `tls_utils._build_cmd`: `if starttls:` replaced with `if starttls is not None:`
26+
for consistency with `probe_raw`.
27+
- `tls_utils._run_openssl`, `_read_server_banner`, `_probe_ssh`, `_probe_ftp`:
28+
`timeout` type annotation corrected from `int` to `float`.
29+
- `cli.py`: `timeout=int(timeout)` cast removed — fractional timeouts are now
30+
forwarded to `assess()` without truncation.
31+
- `reporter.save_report()`: fixed to write from the module-level `_console`
32+
(the recording instance) rather than the public `console` alias, so saved
33+
files always capture the full rendered output.
34+
- `reporter._print_checks_table`, `reporter._print_verdict_panel`: renamed
35+
parameter `con` to `console` for consistency with public API.
36+
- `constants.py`: `import logging as _log` moved to module-level import block;
37+
was previously declared inside an `except Exception:` handler.
38+
- `__init__.py`: `import logging as _logging` moved before the `try/except`
39+
block (PEP 8 — all imports must precede non-import statements).
40+
- `tests/test_tls_utils.py`: `test_returns_none_on_timeout` and
41+
`test_returns_none_on_oserror` now monkeypatch `check_openssl` so they exercise
42+
the subprocess error path deterministically regardless of whether `openssl` is
43+
installed on the test machine.
44+
- `tests/test_tls_utils.py`: extracted `cmd_capture` pytest fixture in
45+
`TestProbeRaw` to replace repeated `captured = {}` / `fake_run` boilerplate
46+
across nine tests; fixture also mocks `check_openssl` for portability.
47+
- `tests/test_cli.py`: `test_exits_2_on_invalid_protocol` renamed to
48+
`test_exits_2_on_missing_target` and rewritten to invoke `check` with no
49+
argument (Typer exit 2) rather than testing a non-existent protocol flag.
50+
- `tests/test_cli.py`: `test_exits_2_on_save_error` no longer mocks
51+
`pathlib.Path.write_text`; relies on a genuinely non-writable path so the
52+
OSError reaches the CLI handler naturally.
53+
- `tests/test_reporter.py`: `_capture` helper now passes the console via
54+
`console=con` keyword argument instead of positional, matching the updated
55+
reporter signature.
56+
57+
### Added
58+
- `tls_utils._VALID_STARTTLS` frozenset — canonical set of modes accepted by
59+
`openssl s_client -starttls`; used to validate `probe_raw`'s `starttls`
60+
parameter.
61+
- `pyproject.toml`: `--cov-fail-under=100` added to `addopts` — 100% coverage
62+
is now enforced by the test runner.
63+
- `tests/test_cli.py`: `TestInfoGroups` class with 5 tests covering the
64+
`info groups` subcommand (exit code, TLS group names, SSH group names,
65+
panel headers). 252 tests total.
66+
1267
---
1368

1469
## [0.6.1] — 2026-06-24

CLAUDE.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
| Language | Python | ≥ 3.11 |
88
| CLI framework | Typer | ≥ 0.12 |
99
| Terminal output | Rich | ≥ 13.7 |
10-
| TLS probe | openssl binary | ≥ 3.0 (external, must be on PATH) |
10+
| TLS probe | openssl binary | ≥ 3.5 (external, must be on PATH) |
1111
| Testing | pytest + pytest-cov | ≥ 8 / ≥ 5 |
1212

1313
## Build & Run
@@ -68,7 +68,7 @@ Two mock targets in tests:
6868
- **Module-level fixtures**: `SAFE_PROBE`, `UNSAFE_PROBE_CLASSICAL`, `UNSAFE_PROBE_TLS12`, `ERROR_PROBE`
6969
- **Assessor mocking**: patch `"quantumvalidator.assessor.check_tls"` (imported at module top)
7070
- **CLI mocking**: patch `"quantumvalidator.assessor.assess"` and `"quantumvalidator.reporter.print_full_report"` (both are lazy imports inside `check()`)
71-
- **Reporter tests**: use `Console(file=StringIO(), no_color=True, width=200)` to capture output; pass console as trailing positional argprivate helpers use `con`, not `console`
71+
- **Reporter tests**: use `Console(file=StringIO(), no_color=True, width=200)` to capture output; pass `console=con` as a keyword argumentall reporter functions (public and private) accept `console=`
7272
- **Test class naming**: `class TestFeatureName:`, snake_case methods, AAA structure
7373
- **Coverage target**: 100% — enforced via `pyproject.toml` addopts
7474

@@ -135,10 +135,8 @@ Every version bump **must** be followed by a GitHub release. Do not leave a vers
135135
git tag vX.Y.Z
136136
git push origin vX.Y.Z
137137

138-
# Create the GitHub release
139-
gh release create vX.Y.Z \
140-
--title "vX.Y.Z" \
141-
--notes "$(cat <<'EOF'
138+
# Create the GitHub release (always use --notes-file, never inline --notes heredoc)
139+
cat > /tmp/release_notes.md << 'ENDOFFILE'
142140
## What's changed
143141
144142
<Copy the ### Added / ### Changed / ### Fixed / ### Removed blocks verbatim
@@ -156,9 +154,9 @@ output-format changes that require user action. Omit for patch releases.>
156154
157155
---
158156
159-
**Full changelog:** https://github.com/NC3-TestingPlatform/quantumvalidator/blob/master/CHANGELOG.md
160-
EOF
161-
)"
157+
**Full changelog:** https://github.com/NC3-TestingPlatform/quantumvalidator/blob/main/CHANGELOG.md
158+
ENDOFFILE
159+
gh release create vX.Y.Z --title "vX.Y.Z" --notes-file /tmp/release_notes.md
162160
```
163161

164162
**Release body checklist:**

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ $ quantumvalidator check cloudflare.com
1212
```
1313

1414
![Python](https://img.shields.io/badge/python-%3E%3D3.11-blue)
15-
![Tests](https://img.shields.io/badge/tests-246%20passing-brightgreen)
15+
![Tests](https://img.shields.io/badge/tests-247%20passing-brightgreen)
1616
![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)
1717
![License](https://img.shields.io/badge/license-GPLv3-lightgrey)
1818

@@ -297,7 +297,7 @@ pytest tests/test_tls_utils.py
297297
pytest tests/test_assessor.py::TestAssessHttps -v
298298
```
299299

300-
The test suite has **246 tests** and maintains **100% statement coverage**.
300+
The test suite has **247 tests** and maintains **100% statement coverage**.
301301

302302
All network I/O (`openssl s_client` subprocess) is mocked at the `probe_tls` boundary —
303303
no test touches a real server or the internet.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,4 @@ dev = ["pytest>=8", "pytest-cov>=5", "pytest-mock>=3.12"]
6464
[tool.pytest.ini_options]
6565
pythonpath = ["."]
6666
testpaths = ["tests"]
67-
addopts = "--cov=quantumvalidator --cov-report=term-missing"
67+
addopts = "--cov=quantumvalidator --cov-report=term-missing --cov-fail-under=100"

quantumvalidator/__init__.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
"""quantumvalidator – quantum-safe cryptography assessment library."""
22

3+
from __future__ import annotations
4+
5+
import logging as _logging
36
from importlib.metadata import PackageNotFoundError, version
47

58
try:
69
__version__ = version("quantumvalidator")
710
except PackageNotFoundError: # pragma: no cover
811
__version__ = "0.6.1"
912

10-
import logging as _logging
11-
1213
_logging.getLogger("quantumvalidator").addHandler(_logging.NullHandler())
1314
del _logging
1415

15-
from quantumvalidator.tls_utils import probe_raw as probe_raw # noqa: F401, E402
16-
17-
__all__ = ["__version__", "probe_raw"]
16+
__all__ = ["__version__"]

quantumvalidator/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def check(
6565
report = assess(
6666
target,
6767
port=port,
68-
timeout=int(timeout),
68+
timeout=timeout,
6969
progress_cb=None if json_output else console.print,
7070
)
7171
except RuntimeError as exc:
@@ -144,7 +144,7 @@ def main(
144144
raise typer.Exit()
145145

146146

147-
def _print_json(report: "QuantumReport") -> None:
147+
def _print_json(report: QuantumReport) -> None:
148148
import json
149149

150150
out = {

quantumvalidator/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from __future__ import annotations
1010

1111
import functools
12+
import logging as _log
1213
import re
1314
import shutil
1415
import subprocess as _sp
@@ -133,6 +134,5 @@ def check_openssl() -> tuple[bool, str]:
133134
"Upgrade OpenSSL (e.g. apt install openssl)."
134135
)
135136
except Exception as exc: # pragma: no cover
136-
import logging as _log
137137
_log.getLogger("quantumvalidator").debug("openssl version parse failed: %s", exc)
138138
return True, ""

quantumvalidator/reporter.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def print_full_report(report: QuantumReport, console: Console | None = None) ->
5959
con.rule("[dim]End of Report[/dim]")
6060

6161

62-
def _print_checks_table(report: QuantumReport, con: Console) -> None:
62+
def _print_checks_table(report: QuantumReport, console: Console) -> None:
6363
table = Table(
6464
box=box.ROUNDED,
6565
show_header=True,
@@ -82,11 +82,11 @@ def _print_checks_table(report: QuantumReport, con: Console) -> None:
8282
check.reason,
8383
)
8484

85-
con.print(table)
86-
con.print()
85+
console.print(table)
86+
console.print()
8787

8888

89-
def _print_verdict_panel(report: QuantumReport, con: Console) -> None:
89+
def _print_verdict_panel(report: QuantumReport, console: Console) -> None:
9090
colour, symbol = _VERDICT_STYLE[report.verdict]
9191

9292
if report.verdict == Verdict.SAFE:
@@ -128,7 +128,7 @@ def _print_verdict_panel(report: QuantumReport, con: Console) -> None:
128128
),
129129
)
130130

131-
con.print(
131+
console.print(
132132
Panel(
133133
body,
134134
title=f"[bold white]Security Verdict:[/bold white] [bold {colour}]{report.verdict.value}[/bold {colour}]",
@@ -137,7 +137,7 @@ def _print_verdict_panel(report: QuantumReport, con: Console) -> None:
137137
padding=(0, 1),
138138
)
139139
)
140-
con.print()
140+
console.print()
141141

142142

143143
# ---------------------------------------------------------------------------
@@ -178,10 +178,10 @@ def save_report(path: str) -> None:
178178
f"Unsupported file extension {ext!r}. Use .txt, .svg, or .html."
179179
)
180180
if fmt == "svg":
181-
console.save_svg(path, clear=False)
181+
_console.save_svg(path, clear=False)
182182
elif fmt == "html":
183-
console.save_html(path, clear=False)
183+
_console.save_html(path, clear=False)
184184
else:
185-
console.save_text(path, clear=False)
185+
_console.save_text(path, clear=False)
186186

187187

0 commit comments

Comments
 (0)