"""Offline V1 wire/identity tests: synthetic devices, no real sockets or upgrades.""" from __future__ import annotations import contextlib import hashlib import importlib.util import io import ipaddress import json from pathlib import Path import struct import tempfile from types import SimpleNamespace import unittest from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] SN = "SYNTHETIC-V1-FULL-SN-0001" MODEL = "SYNTHETIC-V1-MODEL" IP = "192.0.2.10" TARGET = "9.8.7" INFO = 0x5A0E2104 REQUEST = 0x5A0EFF00 RESPONSE = 0x5A0EFF01 SESSION = 1234 def wire(command, body): data = json.dumps(body, separators=(",", ":")).encode("utf-8") return struct.pack("!II", command, len(data)) + data def info_body(**changes): body = dict(code=0, desc="successed", sn=SN, dev_type=MODEL, firmware=TARGET, dev_ver=TARGET, hardware="HW-TEST") body.update(changes) return body def discovery_body(**changes): body = dict(session=SESSION, sn=SN, dev_type=MODEL, ip=IP, mac="00:00:5E:00:53:01", firmware=TARGET, hardware="HW-TEST") body.update(changes) return body def interface(address="192.0.2.20/24"): parsed = ipaddress.ip_interface(address) return SimpleNamespace(address=parsed.ip, prefix_length=parsed.network.prefixlen, network=parsed.network) class TcpSocket: def __init__(self, packet): self.pending = bytearray(packet) self.sent = [] self.timeouts = [] self.closed = False def __enter__(self): return self def __exit__(self, *_args): self.closed = True def settimeout(self, value): self.timeouts.append(value) def sendall(self, packet): self.sent.append(packet) def recv(self, count): count = min(count, 3, len(self.pending)) result = bytes(self.pending[:count]) del self.pending[:count] return result class UdpSocket: def __init__(self, replies=(), bind_failure=False, send_failure=False): self.replies = list(replies) self.bind_failure = bind_failure self.send_failure = send_failure self.sent, self.bindings, self.options = [], [], [] self.closed = False def setsockopt(self, *args): self.options.append(args) def bind(self, address): self.bindings.append(address) if self.bind_failure: raise OSError("synthetic occupied port") def setblocking(self, value): assert value is False def sendto(self, packet, destination): self.sent.append((packet, destination)) if self.send_failure: raise OSError("synthetic route failure") return len(packet) def recvfrom(self, _size): return self.replies.pop(0) def select(self, readable, *_args): assert readable == [self] return ([self] if self.replies else [], [], []) def close(self): self.closed = True class V1ProtocolTests(unittest.TestCase): def setUp(self): spec = importlib.util.spec_from_file_location("adapter_v1_test", ROOT / "scripts" / "device_adapter.py") self.adapter = importlib.util.module_from_spec(spec) spec.loader.exec_module(self.adapter) self.v1 = self.adapter.v1 self.logger = self.adapter.EventCollector() self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.rom = Path(self.temp.name) / "synthetic-v1.rom" self.rom.write_bytes(b"synthetic v1 ROM fixture" * 20) self.sha = hashlib.sha256(self.rom.read_bytes()).hexdigest() def tcp_query(self, body=None, packet=None): sock = TcpSocket(packet if packet is not None else wire(INFO, body or info_body())) with patch.object(self.v1.socket, "create_connection", return_value=sock) as connect: result = self.v1.query_device_info(IP, 5, self.logger) self.assertEqual(connect.call_count, 1) self.assertEqual(connect.call_args.args[0], (IP, 4941)) return result, sock def cli(self, args, body=None): sock = TcpSocket(wire(INFO, body or info_body())) stdout = io.StringIO() with patch.object(self.v1.socket, "create_connection", return_value=sock) as connect, \ patch.object(self.adapter.protocol, "query_device_info", side_effect=AssertionError("V2 forbidden")), \ patch.object(self.adapter.protocol, "transfer_rom", side_effect=AssertionError("transfer forbidden")), \ contextlib.redirect_stdout(stdout): code = self.adapter.main(args + ["--protocol", "v1"]) return code, json.loads(stdout.getvalue()), sock, connect def identity(self): return ["--expected-sn", SN, "--expected-model", MODEL] def discover(self, replies=(), **kwargs): sock = UdpSocket(replies, kwargs.pop("bind_failure", False), kwargs.pop("send_failure", False)) with patch.object(self.v1.socket, "socket", return_value=sock), \ patch.object(self.v1.select, "select", side_effect=sock.select): result = self.v1.discover(kwargs.pop("interfaces", [interface()]), 1, 4942, self.logger, session=SESSION, **kwargs) return result, sock def test_exact_big_endian_empty_info_header(self): self.assertEqual(self.v1.encode_frame(INFO), b"\x5a\x0e\x21\x04\x00\x00\x00\x00") packet = self.v1.encode_frame(REQUEST, {"session": SESSION}) self.assertEqual(packet, wire(REQUEST, {"session": SESSION})) def test_fragmented_tcp_and_documented_field_filter(self): body = info_body() body["undocumented_field"] = {"synthetic": "not retained"} result, sock = self.tcp_query(body) self.assertEqual(sock.sent, [bytes.fromhex("5a0e210400000000")]) self.assertTrue(sock.closed) self.assertEqual(result["data"]["sn"], SN) self.assertNotIn("undocumented_field", result["data"]) self.assertNotIn("undocumented_field", json.dumps(self.logger.events)) def test_decode_rejects_bad_command_length_json_and_utf8(self): cases = [b"1234567", wire(INFO + 1, {}), struct.pack("!II", INFO, 0), struct.pack("!II", INFO, self.v1.MAX_JSON_SIZE + 1), wire(INFO, {}) + b"extra", wire(INFO, []), struct.pack("!II", INFO, 1) + b"{", struct.pack("!II", INFO, 1) + b"\xff"] for packet in cases: with self.subTest(packet=packet[:12]), self.assertRaises(self.v1.ProtocolError): self.v1.decode_frame(packet, INFO) def test_early_tcp_eof_closes_without_retry(self): for packet in [b"", wire(INFO, info_body())[:-1]]: sock = TcpSocket(packet) with patch.object(self.v1.socket, "create_connection", return_value=sock) as connect, \ self.assertRaises(self.v1.ProtocolError): self.v1.query_device_info(IP, 5, self.logger) self.assertEqual(connect.call_count, 1) self.assertTrue(sock.closed) def test_tcp_invalid_headers_are_rejected_before_reading_body(self): for command, size in [(REQUEST, 10), (INFO, 0), (INFO, self.v1.MAX_JSON_SIZE + 1)]: with self.subTest(command=command, size=size), self.assertRaises(self.v1.ProtocolError): self.tcp_query(packet=struct.pack("!II", command, size)) def test_tcp_uses_one_total_deadline_across_fragments(self): sock = TcpSocket(wire(INFO, info_body())) with patch.object(self.v1.socket, "create_connection", return_value=sock) as connect, \ patch.object(self.v1.time, "monotonic", side_effect=[0, 0.1, 0.2, 0.7, 1.1]), \ self.assertRaises(TimeoutError): self.v1.query_device_info(IP, 1, self.logger) self.assertEqual(connect.call_count, 1) self.assertTrue(sock.closed) self.assertEqual(len(sock.sent), 1) def test_native_code_must_be_integer_zero_and_fields_strings(self): for key, value in [("code", False), ("code", "0"), ("code", 1), ("dev_type", {}), ("sn", 123)]: body = info_body() body[key] = value with self.subTest(key=key, value=value), self.assertRaises(self.v1.ProtocolError): self.tcp_query(body) def test_identity_requires_explicit_successful_v1_envelope_and_exact_sn(self): result, _ = self.tcp_query() invalid = [dict(result, code=False), dict(result, code=1), dict(result, protocol="v2"), dict(result, cmd=30101), {"cmd": INFO, "code": 0, "data": result["data"]}] for value in invalid: with self.subTest(value=value), self.assertRaises(self.v1.ProtocolError): self.v1.validate_identity(value, SN, MODEL) with self.assertRaises(self.v1.ProtocolError): self.v1.validate_identity(result, "0001", MODEL) with self.assertRaises(self.v1.ProtocolError): self.v1.validate_identity(result, SN, "wrong-model") def test_dev_id_cannot_substitute_for_dev_type(self): body = info_body() body["dev_type"], body["dev_id"] = "OTHER-MODEL", MODEL result, _ = self.tcp_query(body) with self.assertRaises(self.v1.ProtocolError): self.v1.validate_identity(result, SN, MODEL) def test_identify_observes_model_with_only_full_sn(self): code, result, sock, connect = self.cli(["identify", "--host", IP, "--expected-sn", SN]) self.assertEqual(code, 0) self.assertEqual(result["status"], "IDENTITY_OBSERVED") self.assertEqual(result["models"], [MODEL]) self.assertTrue(result["sn_verified"]) self.assertFalse(result["model_verified"]) self.assertFalse(result["write_executed"]) self.assertEqual(connect.call_args.args[0], (IP, 4941)) self.assertEqual(sock.sent, [bytes.fromhex("5a0e210400000000")]) def test_info_model_mismatch_fails_after_one_read(self): body = info_body() body["dev_type"] = "WRONG-MODEL" code, result, sock, connect = self.cli(["info", "--host", IP, *self.identity()], body) self.assertEqual(code, 1) self.assertEqual(result["status"], "ERROR") self.assertEqual(connect.call_count, 1) self.assertTrue(sock.closed) def test_verify_native_fields_and_missing_outer_without_alias(self): for field, expected in [("firmware", "PASS"), ("dev_ver", "PASS"), ("outer.version", "INCOMPLETE")]: with self.subTest(field=field): code, result, _, _ = self.cli(["verify-version", "--host", IP, *self.identity(), "--target-version", TARGET, "--version-field", field]) self.assertEqual(result["status"], expected) self.assertEqual(result["firmware_verified"], expected == "PASS") self.assertEqual(result["evidence_source"], "live_v1_2104") self.assertEqual(code, 0 if expected == "PASS" else 1) def test_version_mismatch_and_missing_firmware_do_not_use_hardware(self): for actual, status in [("OLD-VERSION", "FAIL"), ("", "INCOMPLETE")]: body = info_body() body.update(firmware=actual, hardware=TARGET) code, result, _, _ = self.cli(["verify-version", "--host", IP, *self.identity(), "--target-version", TARGET, "--version-field", "firmware"], body) self.assertEqual(code, 1) self.assertEqual(result["status"], status) self.assertFalse(result["firmware_verified"]) def test_literal_outer_version_can_be_observed_without_alias_or_other_nested_data(self): for literal, expected in [(TARGET, "PASS"), ("OTHER", "FAIL"), ({}, "INCOMPLETE")]: body = info_body(outer={"version": literal, "unrelated": "discard"}) code, result, _, _ = self.cli(["verify-version", "--host", IP, *self.identity(), "--target-version", TARGET, "--version-field", "outer.version"], body) self.assertEqual(result["status"], expected) self.assertEqual(code, 0 if expected == "PASS" else 1) self.assertNotIn("unrelated", result["response"]["data"].get("outer", {})) def test_recorded_response_requires_v1_envelope_and_no_network(self): path = Path(self.temp.name) / "recorded.json" path.write_text(json.dumps({"cmd": 30101, "code": 0, "data": info_body()}), encoding="utf-8") code, result, _, connect = self.cli(["verify-version", "--response", str(path), *self.identity(), "--target-version", TARGET, "--version-field", "firmware"]) self.assertEqual(code, 1) self.assertEqual(result["status"], "ERROR") self.assertFalse(connect.called) def test_v1_upgrade_preflight_hashes_and_reads_but_never_sends_upgrade(self): code, result, sock, connect = self.cli(["upgrade", "--host", IP, *self.identity(), "--rom", str(self.rom), "--rom-sha256", self.sha, "--target-version", TARGET, "--version-field", "outer.version"]) self.assertEqual(code, 0) self.assertEqual(result["status"], "PREFLIGHT_ONLY") self.assertTrue(result["preflight_complete"]) self.assertFalse(result["write_executed"]) self.assertTrue(result["write_supported"]) self.assertFalse(result["selected_version_field_available"]) self.assertEqual(result["rom"]["sha256"], self.sha) self.assertEqual(connect.call_count, 1) self.assertEqual(sock.sent, [bytes.fromhex("5a0e210400000000")]) def test_udp_request_reply_session_prefix_and_dedup(self): reply = (wire(RESPONSE, discovery_body()), (IP, 4942)) own_request = (wire(REQUEST, {"session": SESSION}), ("192.0.2.20", 4942)) result, sock = self.discover([own_request, reply, reply], expected_sn=SN, interfaces=[interface(), interface("192.0.2.21/24")]) self.assertEqual(sock.bindings, [("0.0.0.0", 4942)]) self.assertEqual(sock.sent, [(wire(REQUEST, {"session": SESSION}), ("192.0.2.255", 4942))]) self.assertTrue(sock.closed) self.assertEqual(result["candidate_count"], 1) self.assertTrue(result["identity_verified"]) self.assertEqual(result["matches"][0]["source_ip"], IP) self.assertIn("v1_own_request_ignored", [event["event"] for event in self.logger.events]) def test_udp_rejects_unrelated_sessions_malformed_mac_and_frames(self): bodies = [discovery_body(session=SESSION + 1), discovery_body(session=True), discovery_body(sn=""), discovery_body(mac=[]), discovery_body(code=False)] replies = [(wire(RESPONSE, body), (IP, 4942)) for body in bodies] replies += [(b"bad", (IP, 4942)), (wire(RESPONSE, discovery_body()), (IP, 4942))] result, _ = self.discover(replies, expected_sn=SN) self.assertTrue(result["identity_verified"]) self.assertEqual(sum(e["event"] == "v1_invalid_response" for e in self.logger.events), 6) def test_udp_same_sn_conflicting_ip_mac_or_model_blocks_selection(self): for body, peer in [(discovery_body(), ("192.0.2.11", 4942)), (discovery_body(mac="00:00:5E:00:53:02"), (IP, 4942)), (discovery_body(dev_type="OTHER-MODEL"), (IP, 4942))]: replies = [(wire(RESPONSE, discovery_body()), (IP, 4942)), (wire(RESPONSE, body), peer)] with self.subTest(body=body, peer=peer): result, _ = self.discover(replies, expected_sn=SN) self.assertEqual(result["status"], "BLOCKED") self.assertEqual(result["matches"], []) self.assertEqual(result["identity_conflicts"], [SN]) def test_udp_exact_sn_and_model_filters_no_suffix_match(self): reply = [(wire(RESPONSE, discovery_body()), (IP, 4942))] for filters in [dict(expected_sn="0001"), dict(expected_sn=SN, expected_model="wrong")]: with self.subTest(filters=filters): result, _ = self.discover(reply, **filters) self.assertEqual(result["status"], "BLOCKED") self.assertFalse(result["identity_verified"]) def test_udp_no_response_is_blocked_for_target_not_an_offline_claim(self): result, sock = self.discover(expected_sn=SN) self.assertEqual(result["status"], "BLOCKED") self.assertEqual(result["reason"], "target_not_uniquely_found") self.assertTrue(sock.closed) def test_udp_port_conflict_and_send_failure_close_without_retry(self): for options in [dict(bind_failure=True), dict(send_failure=True)]: sock = UdpSocket(**options) with patch.object(self.v1.socket, "socket", return_value=sock) as create, \ self.assertRaises(self.v1.ProtocolError): self.v1.discover([interface()], 1, 4942, self.logger, session=SESSION) self.assertEqual(create.call_count, 1) self.assertEqual(len(sock.bindings), 1) self.assertTrue(sock.closed) def test_discovery_cli_routes_actual_v1_frames(self): sock = UdpSocket([(wire(RESPONSE, discovery_body()), (IP, 4942))]) output = io.StringIO() with patch.object(self.v1.socket, "socket", return_value=sock), \ patch.object(self.v1.secrets, "randbelow", return_value=SESSION - 1), \ patch.object(self.v1.select, "select", side_effect=sock.select), contextlib.redirect_stdout(output): code = self.adapter.main(["discover", "--protocol", "v1", "--interface", "192.0.2.20/24", "--expected-sn", SN]) result = json.loads(output.getvalue()) self.assertEqual(code, 0) self.assertTrue(result["identity_verified"]) self.assertEqual(sock.sent[0][0][:4], bytes.fromhex("5a0eff00")) def test_invalid_v1_interface_reports_structured_error_before_network(self): output = io.StringIO() with patch.object(self.v1.socket, "socket", side_effect=AssertionError("No network")), \ contextlib.redirect_stdout(output): code = self.adapter.main(["discover", "--protocol", "v1", "--interface", "invalid"]) self.assertEqual(code, 1) self.assertEqual(json.loads(output.getvalue())["status"], "ERROR") if __name__ == "__main__": unittest.main()