Skip to content

Commit efd2ab3

Browse files
authored
Allow NTAG profile services to default to PC/SC backend (#16)
2 parents fe9c323 + 42f1bed commit efd2ab3

2 files changed

Lines changed: 109 additions & 1 deletion

File tree

src/schnee/services/ntag_profile.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,19 @@ class ReadNtagProfileService(Service[NtagProfile]):
259259
class Request(Service.Request):
260260
"""Request for reading an NTAG profile."""
261261

262+
backend_name: str = Field(
263+
default="pcsc",
264+
description="Backend name, for example `pcsc` or `pcsc:<reader name>`.",
265+
)
266+
262267
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
263268

269+
req: Request
270+
264271
def process(self) -> NtagProfile:
265272
"""Read the current NTAG profile."""
266273
try:
267-
backend = Backend.get(name="pcsc")
274+
backend = Backend.get(self.req.backend_name)
268275
return backend.read_profile()
269276
except Backend.BackendNotFoundError as exc:
270277
raise ReadNtagProfileBackendNotFoundError from exc
@@ -285,6 +292,7 @@ class Request(Service.Request):
285292
"""Request for writing one URL NDEF record."""
286293

287294
backend_name: str = Field(
295+
default="pcsc",
288296
description="Backend name, for example `pcsc` or `pcsc:<reader name>`.",
289297
)
290298
url: str = Field(description="URL to write as a single NDEF URI record.")
@@ -405,6 +413,7 @@ class Request(Service.Request):
405413
"""Request for updating one or more NTAG 424 DNA keys."""
406414

407415
backend_name: str = Field(
416+
default="pcsc",
408417
description="Backend name, for example `pcsc` or `pcsc:<reader name>`.",
409418
)
410419
master_key: bytes = Field(
@@ -482,6 +491,7 @@ class Request(Service.Request):
482491
"""Request for validating one or more NTAG 424 DNA keys."""
483492

484493
backend_name: str = Field(
494+
default="pcsc",
485495
description="Backend name, for example `pcsc` or `pcsc:<reader name>`.",
486496
)
487497
keys: list[Ntag424KeyValidationRequest] = Field(
@@ -541,6 +551,7 @@ class Request(Service.Request):
541551
"""Request for changing NTAG 424 DNA SDM state."""
542552

543553
backend_name: str = Field(
554+
default="pcsc",
544555
description="Backend name, for example `pcsc` or `pcsc:<reader name>`.",
545556
)
546557
master_key: bytes = Field(

tests/schnee/services/test_ntag_profile.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
from schnee.adapters.backend import PcscBackend
77
from schnee.adapters.ntag.apdu import CommandAPDU, ResponseAPDU
88
from schnee.adapters.ntag.core import Ntag424Key, Session
9+
from schnee.adapters.ntag.profile.models import Ntag21xProfile, TagInfo
910
from schnee.services.ntag_profile import (
1011
Ntag424KeyUpdateRequest,
1112
Ntag424KeyValidationRequest,
13+
ReadNtagProfileBackendError,
14+
ReadNtagProfileService,
1215
SetNtag424SdmService,
1316
UpdateNtag424KeysService,
1417
ValidateNtag424KeysService,
@@ -19,6 +22,7 @@
1922
GET_KEY_VERSION_INS = 0x64
2023
EXPECTED_KEY_VERSION = 0x11
2124
MISMATCHED_KEY_VERSION = 0x02
25+
NTAG215_NDEF_CAPACITY_BYTES = 496
2226

2327

2428
class FakeApduClient:
@@ -44,6 +48,99 @@ def send_apdu(
4448
return ResponseAPDU(data=[], sw1=0x90, sw2=0)
4549

4650

51+
def test_read_ntag_profile_request_defaults_to_pcsc() -> None:
52+
"""Profile reads keep using the default PC/SC backend."""
53+
req = ReadNtagProfileService.Request()
54+
55+
assert req.backend_name == "pcsc"
56+
57+
58+
def test_ntag_profile_service_requests_default_to_pcsc() -> None:
59+
"""All NTAG profile services use PC/SC when no backend is specified."""
60+
update = Ntag424KeyUpdateRequest(
61+
key_no=Ntag424Key.APP_MASTER,
62+
new_key=bytes(16),
63+
)
64+
validation = Ntag424KeyValidationRequest(
65+
key_no=Ntag424Key.APP_MASTER,
66+
key=bytes(16),
67+
)
68+
69+
assert WriteNdefUrlService.Request(url="https://example.com").backend_name == "pcsc"
70+
assert (
71+
UpdateNtag424KeysService.Request(
72+
master_key=bytes(16),
73+
updates=[update],
74+
).backend_name
75+
== "pcsc"
76+
)
77+
assert ValidateNtag424KeysService.Request(keys=[validation]).backend_name == "pcsc"
78+
assert (
79+
SetNtag424SdmService.Request(
80+
master_key=bytes(16),
81+
enabled=False,
82+
).backend_name
83+
== "pcsc"
84+
)
85+
86+
87+
def test_read_ntag_profile_service_delegates_to_requested_backend(
88+
monkeypatch: pytest.MonkeyPatch,
89+
) -> None:
90+
"""Profile reads select the backend through the service request."""
91+
profile = Ntag21xProfile(
92+
tag=TagInfo(type="NTAG215", uid="04112233445566"),
93+
capacity_bytes=NTAG215_NDEF_CAPACITY_BYTES,
94+
)
95+
backend_names: list[str] = []
96+
97+
class FakeBackend:
98+
def read_profile(self) -> Ntag21xProfile:
99+
return profile
100+
101+
def get_backend(name: str) -> FakeBackend:
102+
backend_names.append(name)
103+
return FakeBackend()
104+
105+
monkeypatch.setattr("schnee.services.ntag_profile.Backend.get", get_backend)
106+
107+
result = ReadNtagProfileService.call(
108+
ReadNtagProfileService.Request(backend_name="pcsc:Reader A"),
109+
)
110+
111+
assert backend_names == ["pcsc:Reader A"]
112+
assert result is profile
113+
114+
115+
def test_read_ntag_profile_service_translates_backend_errors(
116+
monkeypatch: pytest.MonkeyPatch,
117+
) -> None:
118+
"""Operational backend read failures are exposed through service-level errors."""
119+
120+
class FakeBackend:
121+
def read_profile(self) -> Ntag21xProfile:
122+
msg = "tag profile read failed"
123+
raise PcscBackend.UnsupportedProfileReadError(msg)
124+
125+
monkeypatch.setattr(
126+
"schnee.services.ntag_profile.Backend.get",
127+
lambda _name: FakeBackend(),
128+
)
129+
130+
with pytest.raises(
131+
ReadNtagProfileBackendError,
132+
match=ReadNtagProfileBackendError.msg,
133+
) as exc_info:
134+
ReadNtagProfileService.call(
135+
ReadNtagProfileService.Request(backend_name="pcsc:Reader A"),
136+
)
137+
138+
assert isinstance(
139+
exc_info.value.__cause__,
140+
PcscBackend.UnsupportedProfileReadError,
141+
), "service error should preserve the backend failure as its cause"
142+
143+
47144
def test_ntag424_key_update_request_requires_old_key_for_non_master() -> None:
48145
"""Non-master key updates need the current key for ChangeKey XOR data."""
49146
with pytest.raises(ValidationError, match="old_key"):

0 commit comments

Comments
 (0)