Skip to content

Commit 35afdbc

Browse files
authored
Add backends CLI command (#18)
2 parents 2b53554 + 5780b72 commit 35afdbc

2 files changed

Lines changed: 112 additions & 2 deletions

File tree

src/schnee/controllers/cli/commands.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,29 @@
22

33
from typing import TYPE_CHECKING
44

5+
from schnee.controllers.cli.errors import exit_for_service_error
6+
from schnee.controllers.cli.output import echo_text
7+
from schnee.services.backend import ListBackendNamesService
8+
from schnee.services.base import ServiceError
9+
510
if TYPE_CHECKING:
611
import typer
712

813

9-
def register_commands(_app: typer.Typer) -> None:
14+
def register_commands(app: typer.Typer) -> None:
1015
"""Register CLI commands."""
1116

12-
@_app.callback(invoke_without_command=True)
17+
@app.callback(invoke_without_command=True)
1318
def root() -> None:
1419
"""NFC/RFID tag authentication and encryption tools."""
20+
21+
@app.command("backends")
22+
def backends() -> None:
23+
"""List selectable backend names."""
24+
try:
25+
names = ListBackendNamesService.call(ListBackendNamesService.Request())
26+
except ServiceError as exc:
27+
exit_for_service_error(exc)
28+
29+
for name in names:
30+
echo_text(name)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Tests for the backends CLI command."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
from typing import TYPE_CHECKING, ClassVar
7+
8+
from typer.testing import CliRunner
9+
10+
from schnee.controllers.cli.errors import SERVICE_ERROR_EXIT_CODE
11+
from schnee.controllers.cli.main import app
12+
from schnee.services.base import ServiceError
13+
14+
if TYPE_CHECKING:
15+
import pytest
16+
17+
18+
class ExampleServiceError(ServiceError):
19+
"""Service error used by backends command tests."""
20+
21+
msg: ClassVar[str] = "backend names unavailable"
22+
23+
24+
def test_backends_command_prints_service_backend_names(
25+
monkeypatch: pytest.MonkeyPatch,
26+
) -> None:
27+
"""The backends command delegates backend discovery to the service layer."""
28+
requests: list[object] = []
29+
30+
class FakeListBackendNamesService:
31+
class Request:
32+
"""Request marker used to verify CLI delegation."""
33+
34+
@classmethod
35+
def call(cls, req: object) -> list[str]:
36+
requests.append(req)
37+
return ["pcsc", "pcsc:Reader A"]
38+
39+
monkeypatch.setattr(
40+
"schnee.controllers.cli.commands.ListBackendNamesService",
41+
FakeListBackendNamesService,
42+
)
43+
44+
result = CliRunner().invoke(app, ["backends"], prog_name="schnee")
45+
46+
assert result.exit_code == 0, "backends command should exit successfully"
47+
assert result.output == "pcsc\npcsc:Reader A\n", (
48+
"backends command should print each selectable backend name"
49+
)
50+
assert len(requests) == 1, "backends command should call the service once"
51+
assert isinstance(requests[0], FakeListBackendNamesService.Request), (
52+
"backends command should pass the service request type"
53+
)
54+
55+
56+
def test_backends_command_uses_cli_error_handler_for_service_errors(
57+
monkeypatch: pytest.MonkeyPatch,
58+
) -> None:
59+
"""Service-level failures are rendered through the CLI error handler."""
60+
61+
class FakeListBackendNamesService:
62+
class Request:
63+
"""Request marker used by the fake service."""
64+
65+
@classmethod
66+
def call(cls, req: object) -> list[str]:
67+
_ = req
68+
raise ExampleServiceError
69+
70+
monkeypatch.setattr(
71+
"schnee.controllers.cli.commands.ListBackendNamesService",
72+
FakeListBackendNamesService,
73+
)
74+
75+
result = CliRunner().invoke(app, ["backends"], prog_name="schnee")
76+
77+
assert result.exit_code == SERVICE_ERROR_EXIT_CODE, (
78+
"service errors should terminate with the configured CLI error code"
79+
)
80+
assert ExampleServiceError.msg in result.stderr, (
81+
"service errors should be rendered by the CLI error handler"
82+
)
83+
84+
85+
def test_cli_code_does_not_import_adapter_layer() -> None:
86+
"""CLI modules must not import adapter-layer implementation details."""
87+
cli_dir = Path("src/schnee/controllers/cli")
88+
imported_adapters = [
89+
path
90+
for path in cli_dir.glob("*.py")
91+
if "schnee.adapters" in path.read_text(encoding="utf-8")
92+
]
93+
94+
assert imported_adapters == [], "CLI code should not import schnee.adapters.*"

0 commit comments

Comments
 (0)