Skip to content

Commit 7ac0834

Browse files
authored
Merge pull request #32 from Daylily-Informatics/feature/ip-stub-trailing-dot-validation
feat: reject ip_stub with trailing dot across all entry points
2 parents 2135883 + c9ab162 commit 7ac0834

File tree

9 files changed

+176
-0
lines changed

9 files changed

+176
-0
lines changed

tests/test_cli.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,38 @@ def test_bootstrap_json_hides_scan_progress(self, mock_zpl_cls):
255255
assert "printers_found" in result.output
256256

257257

258+
class TestCLIBootstrapTrailingDot:
259+
"""Tests for trailing-dot ip_stub rejection in bootstrap command."""
260+
261+
def test_bootstrap_rejects_trailing_dot(self):
262+
"""bootstrap --ip-stub '192.168.1.' exits with error."""
263+
result = runner.invoke(app, ["bootstrap", "--ip-stub", "192.168.1."])
264+
assert result.exit_code == 1
265+
assert "trailing dot" in result.output
266+
267+
def test_bootstrap_rejects_trailing_dot_short_flag(self):
268+
"""bootstrap -i '10.0.0.' exits with error."""
269+
result = runner.invoke(app, ["bootstrap", "-i", "10.0.0."])
270+
assert result.exit_code == 1
271+
assert "trailing dot" in result.output
272+
273+
274+
class TestCLIPrinterScanTrailingDot:
275+
"""Tests for trailing-dot ip_stub rejection in printer scan command."""
276+
277+
def test_printer_scan_rejects_trailing_dot(self):
278+
"""printer scan --ip-stub '192.168.1.' exits with error."""
279+
result = runner.invoke(app, ["printer", "scan", "--ip-stub", "192.168.1."])
280+
assert result.exit_code == 1
281+
assert "trailing dot" in result.output
282+
283+
def test_printer_scan_rejects_trailing_dot_short_flag(self):
284+
"""printer scan -i '10.0.0.' exits with error."""
285+
result = runner.invoke(app, ["printer", "scan", "-i", "10.0.0."])
286+
assert result.exit_code == 1
287+
assert "trailing dot" in result.output
288+
289+
258290

259291
# ---------------------------------------------------------------------------
260292
# zday man — interactive documentation browser

tests/test_core_functions.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,51 @@ def test_cache_hit(self):
315315
assert result["online"] is True
316316

317317
clear_printer_cache()
318+
319+
320+
class TestIpStubTrailingDotRejection:
321+
"""Tests for rejecting ip_stub values that end with a trailing dot."""
322+
323+
def test_probe_rejects_trailing_dot(self):
324+
"""probe_zebra_printers_add_to_printers_json raises ValueError for trailing dot."""
325+
import zebra_day.print_mgr as zdpm
326+
327+
zp = zdpm.zpl()
328+
with pytest.raises(ValueError, match="trailing dot"):
329+
zp.probe_zebra_printers_add_to_printers_json(ip_stub="192.168.1.")
330+
331+
def test_probe_rejects_single_dot(self):
332+
"""A bare '.' is also rejected."""
333+
import zebra_day.print_mgr as zdpm
334+
335+
zp = zdpm.zpl()
336+
with pytest.raises(ValueError, match="trailing dot"):
337+
zp.probe_zebra_printers_add_to_printers_json(ip_stub=".")
338+
339+
def test_probe_rejects_multiple_trailing_dots(self):
340+
"""Multiple trailing dots (e.g. '10.0.0..') are rejected."""
341+
import zebra_day.print_mgr as zdpm
342+
343+
zp = zdpm.zpl()
344+
with pytest.raises(ValueError, match="trailing dot"):
345+
zp.probe_zebra_printers_add_to_printers_json(ip_stub="10.0.0..")
346+
347+
def test_probe_accepts_valid_stub(self):
348+
"""Valid ip_stub (no trailing dot) does NOT raise ValueError.
349+
350+
We mock http.client connections to avoid real network I/O.
351+
"""
352+
import zebra_day.print_mgr as zdpm
353+
354+
zp = zdpm.zpl()
355+
# Mock HTTPConnection and HTTPSConnection so the 255-IP loop
356+
# completes instantly without real network calls.
357+
with mock.patch("http.client.HTTPConnection") as mock_http, \
358+
mock.patch("http.client.HTTPSConnection") as mock_https:
359+
# Make every connection attempt raise immediately (no printer)
360+
mock_http.return_value.request.side_effect = OSError("mocked")
361+
mock_https.return_value.request.side_effect = OSError("mocked")
362+
try:
363+
zp.probe_zebra_printers_add_to_printers_json(ip_stub="10.0.0")
364+
except ValueError:
365+
pytest.fail("Valid ip_stub raised ValueError")

tests/test_top_level_api.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,3 +325,26 @@ def test_all_functions_in_all(self):
325325
for func_name in expected_functions:
326326
assert func_name in zd.__all__, f"{func_name} not in __all__"
327327
assert hasattr(zd, func_name), f"{func_name} not accessible on module"
328+
329+
330+
class TestScanIpStubTrailingDot:
331+
"""Tests for trailing-dot ip_stub rejection in the top-level scan() API."""
332+
333+
def setup_method(self):
334+
import zebra_day
335+
336+
zebra_day._reset_zpl()
337+
338+
def test_scan_rejects_trailing_dot(self):
339+
"""scan() raises ValueError before calling the core method."""
340+
import zebra_day as zd
341+
342+
with pytest.raises(ValueError, match="trailing dot"):
343+
zd.scan(ip_stub="192.168.1.")
344+
345+
def test_scan_rejects_trailing_dot_different_prefix(self):
346+
"""scan() rejects any trailing-dot ip_stub."""
347+
import zebra_day as zd
348+
349+
with pytest.raises(ValueError, match="trailing dot"):
350+
zd.scan(ip_stub="10.0.0.")

tests/test_web_server.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,6 +1322,38 @@ def test_import_section_shows_package_templates(self, client):
13221322
assert "import-cb" in response.text
13231323

13241324

1325+
class TestScanTrailingDotRejection:
1326+
"""Tests for trailing-dot ip_stub rejection on web scan routes."""
1327+
1328+
def test_config_scan_rejects_trailing_dot(self, client):
1329+
"""GET /config/scan?ip_stub=192.168.1. returns 400."""
1330+
resp = client.get("/config/scan", params={"ip_stub": "192.168.1."})
1331+
assert resp.status_code == 400
1332+
assert "trailing dot" in resp.json()["detail"]
1333+
1334+
def test_config_scan_stream_rejects_trailing_dot(self, client):
1335+
"""GET /config/scan/stream?ip_stub=10.0.0. returns 400."""
1336+
resp = client.get("/config/scan/stream", params={"ip_stub": "10.0.0."})
1337+
assert resp.status_code == 400
1338+
assert "trailing dot" in resp.json()["detail"]
1339+
1340+
def test_config_scan_accepts_valid_stub(self, client, monkeypatch):
1341+
"""GET /config/scan with valid ip_stub does NOT return 400."""
1342+
zp = client.app.state.zp
1343+
monkeypatch.setattr(
1344+
zp,
1345+
"probe_zebra_printers_add_to_printers_json",
1346+
lambda **kw: None,
1347+
)
1348+
resp = client.get(
1349+
"/config/scan",
1350+
params={"ip_stub": "192.168.1"},
1351+
follow_redirects=False,
1352+
)
1353+
# Should redirect (303) on success, not 400
1354+
assert resp.status_code == 303
1355+
1356+
13251357
# Keep the simple assertion test for backward compatibility
13261358
def test_web_ui():
13271359
"""Simple test to ensure test module loads."""

zebra_day/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,18 @@ def scan(ip_stub: str = "192.168.1", lab: str = "default") -> None:
137137
ip_stub: First three octets of IP range to scan (default: "192.168.1")
138138
lab: Lab identifier to add discovered printers to (default: "default")
139139
140+
Raises:
141+
ValueError: If ip_stub ends with a trailing dot.
142+
140143
Example:
141144
>>> import zebra_day as zd
142145
>>> zd.scan(ip_stub="10.0.0", lab="production")
143146
"""
147+
if isinstance(ip_stub, str) and ip_stub.endswith("."):
148+
raise ValueError(
149+
f"ip_stub must not end with a trailing dot: '{ip_stub}'. "
150+
f"Use '{ip_stub.rstrip('.')}' instead."
151+
)
144152
zp = _get_zpl()
145153
zp.probe_zebra_printers_add_to_printers_json(ip_stub=ip_stub, lab=lab)
146154

zebra_day/cli/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,13 @@ def bootstrap(
215215
except Exception:
216216
ip_stub = "192.168.1"
217217

218+
if ip_stub.endswith("."):
219+
console.print(
220+
f"[red]✗[/red] ip-stub must not end with a trailing dot: '{ip_stub}'. "
221+
f"Use '{ip_stub.rstrip('.')}' instead."
222+
)
223+
raise typer.Exit(1)
224+
218225
if not json_output:
219226
console.print(f"\n[cyan]→[/cyan] Scanning network for Zebra printers ({ip_stub}.*)...")
220227
if silent_scan:

zebra_day/cli/printer.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ def scan(
5050
local_ip = _get_local_ip()
5151
ip_stub = ".".join(local_ip.split(".")[:-1])
5252

53+
if ip_stub.endswith("."):
54+
console.print(
55+
f"[red]✗[/red] ip-stub must not end with a trailing dot: '{ip_stub}'. "
56+
f"Use '{ip_stub.rstrip('.')}' instead."
57+
)
58+
raise typer.Exit(1)
59+
5360
if not json_output:
5461
console.print(f"[cyan]→[/cyan] Scanning {ip_stub}.* for Zebra printers...")
5562
console.print("[dim] This may take a few minutes...[/dim]")

zebra_day/print_mgr.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,13 @@ def probe_zebra_printers_add_to_printers_json(
228228
scan_wait = seconds to re-try probing until moving on. 0.5 default may be too quick/slow
229229
lab = code for the lab key to add/update to given finding new printers
230230
"""
231+
# Reject trailing-dot ip_stub (e.g. "192.168.1." is invalid)
232+
if isinstance(ip_stub, str) and ip_stub.endswith("."):
233+
raise ValueError(
234+
f"ip_stub must not end with a trailing dot: '{ip_stub}'. "
235+
f"Use '{ip_stub.rstrip('.')}' instead."
236+
)
237+
231238
# Ensure schema version is set
232239
if "schema_version" not in self.printers:
233240
self.printers["schema_version"] = "2.1.0"

zebra_day/web/routers/ui.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,12 @@ async def modern_config_scan(
727727
lab: str = "scan-results",
728728
):
729729
"""Scan network for printers."""
730+
if ip_stub.endswith("."):
731+
raise HTTPException(
732+
status_code=400,
733+
detail=f"ip_stub must not end with a trailing dot: '{ip_stub}'. "
734+
f"Use '{ip_stub.rstrip('.')}' instead.",
735+
)
730736
zp = request.app.state.zp
731737
zp.probe_zebra_printers_add_to_printers_json(ip_stub=ip_stub, scan_wait=scan_wait, lab=lab)
732738
time.sleep(2.2)
@@ -741,6 +747,12 @@ async def modern_config_scan_stream(
741747
lab: str = "scan-results",
742748
):
743749
"""Stream network scan progress via Server-Sent Events (SSE)."""
750+
if ip_stub.endswith("."):
751+
raise HTTPException(
752+
status_code=400,
753+
detail=f"ip_stub must not end with a trailing dot: '{ip_stub}'. "
754+
f"Use '{ip_stub.rstrip('.')}' instead.",
755+
)
744756
zp = request.app.state.zp
745757
scan_jobs = _get_scan_jobs(request.app)
746758

0 commit comments

Comments
 (0)