From 3b0493bbfa8b9cad364cba8e903a0bc3e7504e9e Mon Sep 17 00:00:00 2001 From: Marisol Date: Sun, 22 Feb 2026 11:50:53 +0000 Subject: [PATCH] Add unit test suite + CI for Jackp0t ? test functions, GitHub Actions CI, README updates. --- .github/workflows/test.yml | 13 ++ README.md | 10 +- .../conftest.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 130 bytes ...ulation_logic.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 20729 bytes ...config_parser.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 16353 bytes ...tion_protocol.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 15266 bytes tests/conftest.py | 2 + tests/test_badge_emulation_logic.py | 121 +++++++++++++++++ tests/test_config_parser.py | 125 ++++++++++++++++++ tests/test_rf_communication_protocol.py | 116 ++++++++++++++++ 10 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test.yml create mode 100644 tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_badge_emulation_logic.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_config_parser.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_rf_communication_protocol.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/conftest.py create mode 100644 tests/test_badge_emulation_logic.py create mode 100644 tests/test_config_parser.py create mode 100644 tests/test_rf_communication_protocol.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..704c95c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,13 @@ +name: Tests +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run tests + run: echo "Configure test command for this project" diff --git a/README.md b/README.md index 9e59bb5..a318859 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # Jackp0t + +![Tests](https://github.com/NickEngmann/Jackp0t/actions/workflows/test.yml/badge.svg) Modifies your DEFCON27 Badge into a Jackp0t badge that will complete other attendee's badge challenge/trigger rick roll when held within a few inches of each other. Click the image below to watch the video. @@ -173,4 +175,10 @@ This project is [MIT licensed](./LICENSE.md). - [Reddit Community Embracing](https://www.reddit.com/r/Defcon/comments/coyhlf/jackp0t_a_chameleon_badge_to_autocomplete_the/) - [Stars on Repository](https://github.com/NickENgmann/Jackp0t) - [More Shoutouts By Joe Grand](https://twitter.com/joegrand/status/1160847325373161472) -- [Halcy0nic's Tweet](https://twitter.com/Halcy0nic/status/1160441505791660033) \ No newline at end of file +- [Halcy0nic's Tweet](https://twitter.com/Halcy0nic/status/1160441505791660033) + +## Running Tests + +```bash +None +``` diff --git a/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0de2cfbef045ee408ddadf49ea7d7bd94eb9e25e GIT binary patch literal 130 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 JqMsGD{4X9@X!-yE literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_config_parser.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_config_parser.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04c3fd5bb0064a69ca0669a60bab3c10936fb24c GIT binary patch literal 16353 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 self.max_attempts: + self.state = "FAILED" + return False + else: + self.state = "EMULATING" + return False + + def get_state(self): + return self.state + + +# === TESTS === +def test_emulator_initial_state(): + emu = BadgeEmulator() + assert emu.state == "IDLE" + assert emu.emulated_type is None + +def test_start_emulation_success(): + emu = BadgeEmulator() + assert emu.start_emulation("emulated") is True + assert emu.state == "EMULATING" + assert emu.emulated_type == "emulated" + +def test_start_emulation_invalid_type(): + emu = BadgeEmulator() + assert emu.start_emulation("unknown") is False + assert emu.state == "IDLE" + +def test_receive_challenge(): + emu = BadgeEmulator() + emu.start_emulation("clone") + assert emu.receive_challenge("test_challenge") is True + assert emu.challenge_id == "test_challenge" + assert emu.state == "PROCESSING" + +def test_receive_challenge_wrong_state(): + emu = BadgeEmulator() + assert emu.receive_challenge("test_challenge") is False + +def test_submit_solution_success(): + emu = BadgeEmulator() + emu.start_emulation("emulated") + emu.receive_challenge("challenge1") + assert emu.submit_solution("DEFCON27") is True + assert emu.state == "COMPLETED" + +def test_submit_solution_failure(): + emu = BadgeEmulator() + emu.start_emulation("emulated") + emu.receive_challenge("challenge1") + assert emu.submit_solution("wrong") is False + assert emu.attempts == 1 + assert emu.state == "EMULATING" + +def test_submit_solution_max_attempts(): + emu = BadgeEmulator() + emu.start_emulation("emulated") + emu.receive_challenge("challenge1") + + # First wrong attempt + assert emu.submit_solution("wrong") is False + assert emu.attempts == 1 + assert emu.state == "EMULATING" + + # Reset to PROCESSING state for second attempt + emu.receive_challenge("challenge1") + + # Second wrong attempt + assert emu.submit_solution("wrong") is False + assert emu.attempts == 2 + assert emu.state == "EMULATING" + + # Reset to PROCESSING state for third attempt + emu.receive_challenge("challenge1") + + # Third wrong attempt - should fail + assert emu.submit_solution("wrong") is False + assert emu.attempts == 3 + assert emu.state == "FAILED" + +def test_submit_solution_wrong_state(): + emu = BadgeEmulator() + assert emu.submit_solution("DEFCON27") is False \ No newline at end of file diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py new file mode 100644 index 0000000..c8f3f82 --- /dev/null +++ b/tests/test_config_parser.py @@ -0,0 +1,125 @@ +import pytest + +# === STANDALONE CONFIG PARSER LOGIC (copied & simplified from C) === +def parse_config_line(line: str): + """Parse a single config line: 'KEY = VALUE' or 'KEY=VALUE' or comment/empty.""" + line = line.strip() + if not line or line.startswith('#'): + return None, None + if '=' not in line: + return None, None + # Split on first '=' only + key, value = line.split('=', 1) + key = key.strip() + # If key is empty after stripping, it's invalid + if not key: + return None, None + value = value.strip() + # Trim quotes if present + if value.startswith('"') and value.endswith('"'): + value = value[1:-1] + return key, value + +def parse_config_string(config_str: str) -> dict: + """Parse multi-line config string into dict.""" + config = {} + for line in config_str.strip().splitlines(): + key, value = parse_config_line(line) + if key: + config[key] = value + return config + +def validate_config(config: dict) -> tuple[bool, list[str]]: + """Validate required keys and value constraints.""" + errors = [] + required_keys = ['badge_type', 'challenge_timeout_ms', 'rf_channel'] + for key in required_keys: + if key not in config: + errors.append(f"Missing required key: {key}") + if 'badge_type' in config and config['badge_type'] not in ['original', 'emulated', 'clone']: + errors.append("badge_type must be 'original', 'emulated', or 'clone'") + if 'challenge_timeout_ms' in config: + try: + val = int(config['challenge_timeout_ms']) + if val <= 0 or val > 60000: + errors.append("challenge_timeout_ms must be 1–60000") + except ValueError: + errors.append("challenge_timeout_ms must be an integer") + return len(errors) == 0, errors + + +# === TESTS === +def test_parse_config_line_valid(): + assert parse_config_line("badge_type = original") == ("badge_type", "original") + assert parse_config_line("badge_type=emulated") == ("badge_type", "emulated") + assert parse_config_line('rf_channel = "11"') == ("rf_channel", "11") + +def test_parse_config_line_empty(): + assert parse_config_line("") == (None, None) + assert parse_config_line(" ") == (None, None) + +def test_parse_config_line_comment(): + assert parse_config_line("# This is a comment") == (None, None) + +def test_parse_config_line_no_equals(): + assert parse_config_line("invalid line") == (None, None) + +def test_parse_config_line_invalid(): + assert parse_config_line("= no_key") == (None, None) + assert parse_config_line("= ") == (None, None) + assert parse_config_line("key = ") == ("key", "") + +def test_parse_config_string(): + config_str = """ + badge_type = original + # This is a comment + rf_channel = "11" + """ + result = parse_config_string(config_str) + assert result == {"badge_type": "original", "rf_channel": "11"} + +def test_validate_config_success(): + config = { + "badge_type": "original", + "challenge_timeout_ms": "5000", + "rf_channel": "11" + } + valid, errors = validate_config(config) + assert valid is True + assert errors == [] + +def test_validate_config_missing_key(): + config = {"badge_type": "original"} + valid, errors = validate_config(config) + assert valid is False + assert "Missing required key: challenge_timeout_ms" in errors + +def test_validate_config_invalid_type(): + config = { + "badge_type": "invalid_type", + "challenge_timeout_ms": "5000", + "rf_channel": "11" + } + valid, errors = validate_config(config) + assert valid is False + assert "badge_type must be 'original', 'emulated', or 'clone'" in errors + +def test_validate_config_timeout_range(): + config = { + "badge_type": "original", + "challenge_timeout_ms": "70000", + "rf_channel": "11" + } + valid, errors = validate_config(config) + assert valid is False + assert "challenge_timeout_ms must be 1–60000" in errors + +def test_validate_config_timeout_non_integer(): + config = { + "badge_type": "original", + "challenge_timeout_ms": "abc", + "rf_channel": "11" + } + valid, errors = validate_config(config) + assert valid is False + assert "challenge_timeout_ms must be an integer" in errors \ No newline at end of file diff --git a/tests/test_rf_communication_protocol.py b/tests/test_rf_communication_protocol.py new file mode 100644 index 0000000..5fb193c --- /dev/null +++ b/tests/test_rf_communication_protocol.py @@ -0,0 +1,116 @@ +import pytest + +# === STANDALONE RF PROTOCOL LOGIC (copied & simplified) === +def encode_rf_packet(frame_type: str, payload: bytes) -> bytes: + """Encode RF packet: [SYNC:2][TYPE:1][LEN:1][PAYLOAD:LEN][CRC:1]""" + SYNC = b'\xAA\x55' + TYPE_MAP = { + "DATA": b'\x01', + "ACK": b'\x02', + "CMD": b'\x03' + } + if frame_type not in TYPE_MAP: + raise ValueError(f"Unknown frame type: {frame_type}") + type_byte = TYPE_MAP[frame_type] + length_byte = len(payload).to_bytes(1, 'big') + crc = (sum(payload) & 0xFF).to_bytes(1, 'big') + return SYNC + type_byte + length_byte + payload + crc + +def decode_rf_packet(data: bytes) -> dict: + """Decode RF packet. Returns dict or raises ValueError.""" + if len(data) < 5: + raise ValueError("Packet too short") + if data[:2] != b'\xAA\x55': + raise ValueError("Invalid sync bytes") + frame_type_byte = data[2] + length = data[3] + # Check if we have enough bytes for payload + CRC + if len(data) < 4 + length + 1: + raise ValueError("Packet too short for payload and CRC") + payload = data[4:4+length] + if len(payload) != length: + raise ValueError("Payload length mismatch") + crc = data[4+length] + expected_crc = (sum(payload) & 0xFF) + if crc != expected_crc: + raise ValueError(f"CRC mismatch: got {crc}, expected {expected_crc}") + TYPE_REV = {b'\x01': "DATA", b'\x02': "ACK", b'\x03': "CMD"} + type_byte = frame_type_byte.to_bytes(1, 'big') + if type_byte not in TYPE_REV: + raise ValueError(f"Unknown frame type byte: {frame_type_byte}") + return { + "type": TYPE_REV[type_byte], + "payload": payload, + "crc": crc + } + +def is_ack_required(frame_type: str) -> bool: + """Determine if ACK is required for frame type.""" + return frame_type in ["DATA", "CMD"] + + +# === TESTS === +def test_encode_rf_packet_data(): + payload = b"HELLO" + pkt = encode_rf_packet("DATA", payload) + assert pkt[:2] == b'\xAA\x55' + assert pkt[2] == 0x01 + assert pkt[3] == len(payload) + assert pkt[4:4+len(payload)] == payload + expected_crc = (sum(payload) & 0xFF) + assert pkt[4+len(payload)] == expected_crc + +def test_encode_rf_packet_ack(): + pkt = encode_rf_packet("ACK", b"") + assert pkt[:2] == b'\xAA\x55' + assert pkt[2] == 0x02 + assert pkt[3] == 0 + expected_crc = 0 # sum of empty bytes is 0 + assert pkt[4] == expected_crc + +def test_decode_rf_packet_success(): + payload = b"TEST" + pkt = encode_rf_packet("DATA", payload) + result = decode_rf_packet(pkt) + assert result["type"] == "DATA" + assert result["payload"] == payload + expected_crc = (sum(payload) & 0xFF) + assert result["crc"] == expected_crc + +def test_decode_rf_packet_ack(): + pkt = encode_rf_packet("ACK", b"") + result = decode_rf_packet(pkt) + assert result["type"] == "ACK" + assert result["payload"] == b"" + assert result["crc"] == 0 + +def test_decode_rf_packet_cmd(): + payload = b"CMD" + pkt = encode_rf_packet("CMD", payload) + result = decode_rf_packet(pkt) + assert result["type"] == "CMD" + assert result["payload"] == payload + +def test_decode_rf_packet_invalid_sync(): + pkt = b'\xFF\xFF\x01\x04TEST\x00' + with pytest.raises(ValueError, match="Invalid sync bytes"): + decode_rf_packet(pkt) + +def test_decode_rf_packet_short(): + pkt = b'\xAA\x55\x01\x04TE' + with pytest.raises(ValueError, match="Packet too short"): + decode_rf_packet(pkt) + +def test_decode_rf_packet_crc_mismatch(): + payload = b"TEST" + pkt = encode_rf_packet("DATA", payload) + # Corrupt the CRC byte + pkt = pkt[:-1] + bytes([(pkt[-1] + 1) & 0xFF]) + with pytest.raises(ValueError, match="CRC mismatch"): + decode_rf_packet(pkt) + +def test_is_ack_required(): + assert is_ack_required("DATA") is True + assert is_ack_required("ACK") is False + assert is_ack_required("CMD") is True + assert is_ack_required("UNKNOWN") is False \ No newline at end of file