66from schnee .adapters .backend import PcscBackend
77from schnee .adapters .ntag .apdu import CommandAPDU , ResponseAPDU
88from schnee .adapters .ntag .core import Ntag424Key , Session
9+ from schnee .adapters .ntag .profile .models import Ntag21xProfile , TagInfo
910from schnee .services .ntag_profile import (
1011 Ntag424KeyUpdateRequest ,
1112 Ntag424KeyValidationRequest ,
13+ ReadNtagProfileBackendError ,
14+ ReadNtagProfileService ,
1215 SetNtag424SdmService ,
1316 UpdateNtag424KeysService ,
1417 ValidateNtag424KeysService ,
1922GET_KEY_VERSION_INS = 0x64
2023EXPECTED_KEY_VERSION = 0x11
2124MISMATCHED_KEY_VERSION = 0x02
25+ NTAG215_NDEF_CAPACITY_BYTES = 496
2226
2327
2428class 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+
47144def 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