Skip to content

Commit 7d25bc0

Browse files
Copilotbofh69
andcommitted
Add tests for NDEF parsing methods (#34)
* Initial plan * Add comprehensive tests for decode_cc and get_ndef methods Co-authored-by: bofh69 <1444315+bofh69@users.noreply.github.com> * Refactor tests to use pytest fixtures Co-authored-by: bofh69 <1444315+bofh69@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bofh69 <1444315+bofh69@users.noreply.github.com>
1 parent 8bf84bb commit 7d25bc0

File tree

3 files changed

+609
-1
lines changed

3 files changed

+609
-1
lines changed

src/pn5180_tagomatic/iso14443a.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ def decode_cc(self, cc: bytes) -> tuple[int, int, int, bool] | None:
288288

289289
mlen = (cc[2]) * 4
290290

291-
is_readonly = bool((cc[3] & 0xF0) == 0xF)
291+
is_readonly = bool((cc[3] & 0xF0) == 0xF0)
292292

293293
return (major, minor, mlen, is_readonly)
294294

tests/test_iso14443a_ndef.py

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
# SPDX-FileCopyrightText: 2026 PN5180-tagomatic contributors
2+
# SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
"""Tests for ISO14443a card NDEF functionality."""
5+
6+
from unittest.mock import MagicMock
7+
8+
import pytest
9+
10+
from pn5180_tagomatic.cards import Iso14443AUniqueId
11+
from pn5180_tagomatic.iso14443a import ISO14443ACard
12+
13+
14+
@pytest.fixture
15+
def iso14443a_card():
16+
"""Create an ISO14443A card instance for testing."""
17+
mock_comm = MagicMock()
18+
uid = Iso14443AUniqueId(bytes([0x01, 0x02, 0x03, 0x04]), bytes([0x08]))
19+
return ISO14443ACard(mock_comm, uid)
20+
21+
22+
def test_iso14443a_decode_cc_valid(iso14443a_card):
23+
"""Test decode_cc with valid capability container."""
24+
# Valid CC:
25+
# * magic byte 0xE1
26+
# * version 1.0
27+
# * memory size 12*4=48 bytes
28+
# * read/write access
29+
cc = bytes([0xE1, 0x10, 0x0C, 0x00])
30+
31+
result = iso14443a_card.decode_cc(cc)
32+
33+
assert result is not None
34+
major, minor, mlen, is_readonly = result
35+
assert major == 1
36+
assert minor == 0
37+
assert mlen == 48 # 12 * 4
38+
assert is_readonly is False
39+
40+
41+
def test_iso14443a_decode_cc_readonly(iso14443a_card):
42+
"""Test decode_cc with readonly access."""
43+
# CC with readonly flag (cc[3] & 0xF0 == 0xF0)
44+
cc = bytes([0xE1, 0x10, 0x0C, 0xF0])
45+
46+
result = iso14443a_card.decode_cc(cc)
47+
48+
assert result is not None
49+
major, minor, mlen, is_readonly = result
50+
assert major == 1
51+
assert minor == 0
52+
assert mlen == 48
53+
assert is_readonly is True
54+
55+
56+
def test_iso14443a_decode_cc_partial_readonly(iso14443a_card):
57+
"""Test decode_cc with partial readonly bits set."""
58+
# CC with some readonly bits but not all (0xF0)
59+
cc = bytes([0xE1, 0x10, 0x0C, 0xE0])
60+
61+
result = iso14443a_card.decode_cc(cc)
62+
63+
assert result is not None
64+
major, minor, mlen, is_readonly = result
65+
assert is_readonly is False # Not all bits set
66+
67+
68+
def test_iso14443a_decode_cc_invalid_magic(iso14443a_card):
69+
"""Test decode_cc with invalid magic byte."""
70+
# Invalid magic byte (should be 0xE1)
71+
cc = bytes([0xE2, 0x10, 0x0C, 0x00])
72+
73+
result = iso14443a_card.decode_cc(cc)
74+
75+
assert result is None
76+
77+
78+
def test_iso14443a_decode_cc_short_input(iso14443a_card):
79+
"""Test decode_cc with input too short."""
80+
# Too short (only 3 bytes, should be at least 4)
81+
cc = bytes([0xE1, 0x10, 0x0C])
82+
83+
with pytest.raises(ValueError):
84+
iso14443a_card.decode_cc(cc)
85+
86+
87+
def test_iso14443a_get_ndef_simple_tlv(iso14443a_card):
88+
"""Test get_ndef with a simple NDEF TLV structure."""
89+
# Memory layout for ISO14443a:
90+
# Bytes 0-11: Not used (12 bytes before CC)
91+
# Bytes 12-15: CC (0xE1, version 1.0, 48 bytes, read/write)
92+
# Bytes 16-17: NDEF TLV (Type=0x03, Length=10)
93+
# Bytes 18-27: NDEF message content
94+
memory = bytearray([0x00] * 12) # First 12 bytes
95+
memory.extend(
96+
[
97+
0xE1,
98+
0x10,
99+
0x0C,
100+
0x00, # CC at offset 12
101+
0x03,
102+
0x0A, # NDEF TLV at offset 16
103+
0xD1,
104+
0x01,
105+
0x06,
106+
0x54,
107+
0x02,
108+
0x65,
109+
0x6E,
110+
0x68,
111+
0x69,
112+
0x00, # NDEF message
113+
]
114+
)
115+
# Pad to 48 bytes to match CC
116+
memory.extend([0x00] * (48 - len(memory)))
117+
118+
result = iso14443a_card.get_ndef(memory)
119+
120+
assert result is not None
121+
pos, ndef_bytes = result
122+
assert pos == 18
123+
assert len(ndef_bytes) == 10
124+
assert ndef_bytes == bytes(
125+
[0xD1, 0x01, 0x06, 0x54, 0x02, 0x65, 0x6E, 0x68, 0x69, 0x00]
126+
)
127+
128+
129+
def test_iso14443a_get_ndef_long_length(iso14443a_card):
130+
"""Test get_ndef with 3-byte length encoding (length >= 255)."""
131+
# Memory with 3-byte length encoding
132+
memory = bytearray([0x00] * 12) # First 12 bytes
133+
memory.extend(
134+
[
135+
0xE1,
136+
0x10,
137+
0xFF,
138+
0x00, # CC: 255*4=1020 bytes
139+
0x03,
140+
0xFF,
141+
0x01,
142+
0x00, # NDEF TLV: Type=0x03, Length=0xFF (3-byte), value=256
143+
]
144+
)
145+
# Add 256 bytes of NDEF data
146+
memory.extend([0xBB] * 256)
147+
# Pad to 1020 bytes to match CC
148+
memory.extend([0x00] * (1020 - len(memory)))
149+
150+
result = iso14443a_card.get_ndef(memory)
151+
152+
assert result is not None
153+
pos, ndef_bytes = result
154+
assert pos == 20
155+
assert len(ndef_bytes) == 256
156+
assert all(b == 0xBB for b in ndef_bytes)
157+
158+
159+
def test_iso14443a_get_ndef_with_null_tlvs(iso14443a_card):
160+
"""Test get_ndef with NULL TLVs before NDEF."""
161+
# Memory with NULL TLVs (0x00) before NDEF
162+
memory = bytearray([0x00] * 12)
163+
memory.extend(
164+
[
165+
0xE1,
166+
0x10,
167+
0x0C,
168+
0x00, # CC
169+
0x00,
170+
0x00, # NULL TLVs
171+
0x03,
172+
0x05, # NDEF TLV
173+
0x48,
174+
0x65,
175+
0x6C,
176+
0x6C,
177+
0x6F, # "Hello"
178+
]
179+
)
180+
# Pad to 48 bytes to match CC
181+
memory.extend([0x00] * (48 - len(memory)))
182+
183+
result = iso14443a_card.get_ndef(memory)
184+
185+
assert result is not None
186+
pos, ndef_bytes = result
187+
assert pos == 20
188+
assert ndef_bytes == bytes([0x48, 0x65, 0x6C, 0x6C, 0x6F])
189+
190+
191+
def test_iso14443a_get_ndef_terminator_tlv(iso14443a_card):
192+
"""Test get_ndef returns None when encountering terminator TLV before NDEF."""
193+
# Memory with terminator TLV (0xFE) before NDEF
194+
memory = bytearray([0x00] * 12)
195+
memory.extend(
196+
[
197+
0xE1,
198+
0x10,
199+
0x0C,
200+
0x00, # CC
201+
0xFE, # Terminator TLV
202+
0x03,
203+
0x05, # NDEF TLV (won't be reached)
204+
0x48,
205+
0x65,
206+
0x6C,
207+
0x6C,
208+
0x6F,
209+
]
210+
)
211+
212+
result = iso14443a_card.get_ndef(memory)
213+
214+
assert result is None
215+
216+
217+
def test_iso14443a_get_ndef_invalid_cc(iso14443a_card):
218+
"""Test get_ndef returns None with invalid CC."""
219+
# Invalid CC (wrong magic byte)
220+
memory = bytearray([0x00] * 12)
221+
memory.extend(
222+
[
223+
0xE2,
224+
0x10,
225+
0x0C,
226+
0x00, # Invalid CC
227+
0x03,
228+
0x05,
229+
0x48,
230+
0x65,
231+
0x6C,
232+
0x6C,
233+
0x6F,
234+
]
235+
)
236+
237+
result = iso14443a_card.get_ndef(memory)
238+
239+
assert result is None
240+
241+
242+
def test_iso14443a_get_ndef_unsupported_version(iso14443a_card):
243+
"""Test get_ndef returns None with unsupported major version."""
244+
# Major version > 1 (unsupported for ISO14443a)
245+
memory = bytearray([0x00] * 12)
246+
memory.extend(
247+
[
248+
0xE1,
249+
0x20,
250+
0x0C,
251+
0x00, # CC with major version 2
252+
0x03,
253+
0x05,
254+
0x48,
255+
0x65,
256+
0x6C,
257+
0x6C,
258+
0x6F,
259+
]
260+
)
261+
262+
result = iso14443a_card.get_ndef(memory)
263+
264+
assert result is None
265+
266+
267+
def test_iso14443a_get_ndef_memory_too_small(iso14443a_card):
268+
"""Test get_ndef returns None when memory is smaller than CC indicates."""
269+
# CC indicates 1020 bytes but memory is only 28 bytes
270+
memory = bytearray([0x00] * 12)
271+
memory.extend(
272+
[
273+
0xE1,
274+
0x10,
275+
0xFF,
276+
0x00, # CC: 255*4=1020 bytes
277+
0x03,
278+
0x05,
279+
0x48,
280+
0x65,
281+
0x6C,
282+
0x6C,
283+
0x6F,
284+
]
285+
)
286+
287+
result = iso14443a_card.get_ndef(memory)
288+
289+
assert result is None
290+
291+
292+
def test_iso14443a_get_ndef_field_exceeds_memory(iso14443a_card):
293+
"""Test get_ndef returns None when NDEF field exceeds memory length."""
294+
# NDEF field length exceeds available memory
295+
memory = bytearray([0x00] * 12)
296+
memory.extend(
297+
[
298+
0xE1,
299+
0x10,
300+
0x0C,
301+
0x00, # CC: 48 bytes
302+
0x03,
303+
0x64, # NDEF TLV: Length=100 (exceeds limit)
304+
0x48,
305+
0x65,
306+
0x6C,
307+
0x6C,
308+
0x6F,
309+
]
310+
)
311+
312+
result = iso14443a_card.get_ndef(memory)
313+
314+
assert result is None

0 commit comments

Comments
 (0)