"""Offline cross-compiler tests: no hardware, network, or real credentials.""" import hashlib import http.client import io import json import os from pathlib import Path import re import ssl import subprocess import tempfile import unittest ROOT = Path(__file__).resolve().parent COMPILER = (Path.home() / ".platformio" / "packages" / "toolchain-xtensa-esp32s3" / "bin" / ("xtensa-esp32s3-elf-g++.exe" if os.name == "nt" else "xtensa-esp32s3-elf-g++")) HEADER = ROOT / "include" / "cloud_config.h" TOKEN = "a" * 64 # Synthetic test value, never registered in any hub. class CloudIdentityTests(unittest.TestCase): def compile(self, identity, token=TOKEN, expected=None): if not COMPILER.is_file(): self.skipTest("ESP32-S3 toolchain not installed") # Isolate header lookup: never read a developer's private credential file. with tempfile.TemporaryDirectory(prefix="cloud-id-test-") as directory: Path(directory, "cloud_config.h").write_bytes(HEADER.read_bytes()) arguments = [str(COMPILER), "-std=gnu++11", "-fsyntax-only", "-x", "c++", "-", "-I", directory] if identity is not None: arguments.append("-DPCB_ID=" + identity) if token is not None: arguments.append('-DCLOUD_DEVICE_TOKEN="' + token + '"') source = '#include "cloud_config.h"\n' if expected is not None: source += ( "constexpr bool same(const char* a, const char* b) { " "return *a == *b && (*a == '\\0' || same(a + 1, b + 1)); }\n" 'static_assert(same(CLOUD_DEVICE_ID, "' + expected + '"), "ID mismatch");\n' ) return subprocess.run(arguments, input=source, text=True, capture_output=True, timeout=30, cwd=directory) def test_01_exact_hub_ids(self): for value, expected in (("0x01", "esp32-000000000001"), ("0x02", "esp32-000000000002"), ("0xabcdef123456ULL", "esp32-abcdef123456"), ("0xffffffffffffULL", "esp32-ffffffffffff")): with self.subTest(value=value): result = self.compile(value, expected=expected) self.assertEqual(result.returncode, 0, result.stderr) def test_02_invalid_ids_cannot_produce_firmware(self): for value in (None, "0", "-1", "0x1000000000000ULL", "1.5"): with self.subTest(value=value): result = self.compile(value) self.assertNotEqual(result.returncode, 0) self.assertIn("PCB_ID", result.stderr) def test_03_missing_or_invalid_token_is_a_build_error(self): for token in (None, "", "a" * 63, "a" * 65, "G" * 64, "A" * 64): with self.subTest(length=None if token is None else len(token)): result = self.compile("0x01", token) self.assertNotEqual(result.returncode, 0) self.assertIn("CLOUD_DEVICE_TOKEN", result.stderr) def test_04_root_certificate_matches_isrg_root_x1(self): text = HEADER.read_text(encoding="utf-8") pem = re.search(r"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----", text, re.S).group() digest = hashlib.sha256(ssl.PEM_cert_to_DER_cert(pem)).hexdigest() self.assertEqual(digest, "96bcec06264976f37460779acf28c5a7cfe8a3c0aae11a8ffcee05c0bddf08c6") def test_05_transport_keeps_token_out_of_url_and_verifies_tls(self): text = (ROOT / "src" / "cloud_client.cpp").read_text(encoding="utf-8") self.assertIn('"?device_id="', text) self.assertIn("devicePath += CLOUD_DEVICE_ID", text) self.assertIn('setExtraHeaders("Authorization: Bearer " CLOUD_DEVICE_TOKEN)', text) self.assertIn("beginSslWithCA(host, port, devicePath.c_str(), CLOUD_ROOT_CA_PEM)", text) self.assertIn("client.setCACert(CLOUD_ROOT_CA_PEM)", text) self.assertNotIn("setInsecure()", text) def test_06_websocket_headers_have_no_bytes_after_http_terminator(self): text = (ROOT / "src/cloud_client.cpp").read_text(encoding="utf-8") arguments = re.search(r"setExtraHeaders\((.*?)\);", text, re.S).group(1) before, after = arguments.split("CLOUD_DEVICE_TOKEN") def literals(fragment): return "".join(json.loads(value) for value in re.findall(r'"(?:[^"\\]|\\.)*"', fragment)) extra = literals(before) + TOKEN + literals(after) # WebSockets 2.7.3 appends CRLF to extraHeaders, then User-Agent, then CRLF. request = ( "GET /ws/esp32?device_id=esp32-000000000002 HTTP/1.1\r\n" "Host: micro.ferrasdh.uk:443\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n" "Sec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + extra + "\r\nUser-Agent: arduino-WebSocket-Client\r\n\r\n" ).encode("ascii") stream = io.BytesIO(request) stream.readline() headers = http.client.parse_headers(stream) self.assertEqual(headers.get("Authorization"), "Bearer " + TOKEN) self.assertEqual(headers.get("User-Agent"), "arduino-WebSocket-Client") self.assertEqual(stream.read(), b"", "HTTP header bytes would become invalid WebSocket frames") if __name__ == "__main__": unittest.main(verbosity=2)