"""Tests for access control I/O: credentials, Wiegand, OSDP.""" import asyncio import pytest from pm3py.sim.access_control.credential import ( Credential, encode_wiegand, decode_wiegand, from_uid, ) from pm3py.sim.access_control.wiegand import ( WiegandOutput, WiegandInput, ) from pm3py.sim.access_control.osdp import ( OSDPFrame, OSDPChannel, osdp_crc16, ) def run(coro): return asyncio.get_event_loop().run_until_complete(coro) # --------------------------------------------------------------------------- # Credential model # --------------------------------------------------------------------------- class TestCredential: def test_construction(self): c = Credential(facility_code=42, card_number=12345, format="H10301") assert c.facility_code == 42 assert c.card_number == 12345 assert c.format == "H10301" def test_equality(self): c1 = Credential(42, 12345, "H10301") c2 = Credential(42, 12345, "H10301") assert c1 == c2 def test_from_uid_derives_credential(self): """Derive a credential from a tag UID.""" cred = from_uid(b"\x01\x02\x03\x04", format="H10301") assert cred.facility_code >= 0 assert cred.card_number >= 0 assert cred.format == "H10301" class TestWiegandEncoding: def test_encode_26bit(self): bits = encode_wiegand(Credential(42, 12345, "H10301")) assert len(bits) == 26 def test_decode_26bit(self): cred = Credential(42, 12345, "H10301") bits = encode_wiegand(cred) decoded = decode_wiegand(bits, "H10301") assert decoded.facility_code == 42 assert decoded.card_number == 12345 def test_encode_37bit(self): bits = encode_wiegand(Credential(1000, 50000, "H10304")) assert len(bits) == 37 def test_decode_37bit_roundtrip(self): cred = Credential(1000, 50000, "H10304") bits = encode_wiegand(cred) decoded = decode_wiegand(bits, "H10304") assert decoded == cred def test_26bit_parity(self): bits = encode_wiegand(Credential(42, 12345, "H10301")) assert sum(bits[:13]) % 2 == 0 # even parity first half assert sum(bits[13:]) % 2 == 1 # odd parity second half def test_encode_35bit_corp1000(self): bits = encode_wiegand(Credential(100, 5000, "C1000_35")) assert len(bits) == 35 # --------------------------------------------------------------------------- # Wiegand I/O (software mode) # --------------------------------------------------------------------------- class TestWiegandOutput: def test_software_mode_encode(self): """WiegandOutput in software mode stores bit stream.""" out = WiegandOutput() cred = Credential(42, 12345, "H10301") out.send(cred) assert len(out.last_bits) == 26 def test_software_mode_timing(self): """Verify pulse timing parameters exist.""" out = WiegandOutput(pulse_width_us=50, interval_us=2000) assert out.pulse_width_us == 50 assert out.interval_us == 2000 class TestWiegandInput: def test_software_mode_decode(self): """WiegandInput in software mode decodes bit stream.""" inp = WiegandInput() cred = Credential(42, 12345, "H10301") bits = encode_wiegand(cred) decoded = inp.decode(bits, "H10301") assert decoded.facility_code == 42 assert decoded.card_number == 12345 # --------------------------------------------------------------------------- # OSDP protocol # --------------------------------------------------------------------------- class TestOSDPFrame: def test_frame_construction(self): frame = OSDPFrame(address=0, command=0x60, data=b"") raw = frame.encode() assert raw[0] == 0x53 # SOM assert len(raw) >= 7 # SOM + ADDR + LEN(2) + CMD + CRC(2) def test_frame_parse_roundtrip(self): frame = OSDPFrame(address=1, command=0x61, data=b"\x01\x02") raw = frame.encode() parsed = OSDPFrame.decode(raw) assert parsed.address == 1 assert parsed.command == 0x61 assert parsed.data == b"\x01\x02" def test_crc16(self): """OSDP uses CRC-16/AUG-CCITT.""" crc = osdp_crc16(b"\x53\x00\x08\x00\x60") assert isinstance(crc, int) assert 0 <= crc <= 0xFFFF class TestOSDPChannel: def test_software_loopback(self): """Channel in software mode: controller polls, reader replies.""" channel = OSDPChannel() # software loopback # Controller sends POLL poll = OSDPFrame(address=0, command=0x60, data=b"") channel.send(poll) # Reader responds with ACK ack = OSDPFrame(address=0, command=0x40, data=b"") channel.send(ack) # Both frames in buffer assert len(channel.buffer) == 2 def test_card_present_notification(self): """Simulate card present event via OSDP.""" channel = OSDPChannel() cred = Credential(42, 12345, "H10301") bits = encode_wiegand(cred) # Reader sends RAW (card data) — cmd 0x73 raw_data = bytes(bits) # simplified frame = OSDPFrame(address=0, command=0x73, data=raw_data) channel.send(frame) last = channel.buffer[-1] assert last.command == 0x73