Files
framebridge/source/tests/test_fleet_broker_node.py
T

145 lines
6.1 KiB
Python

"""Real FBPAIR/1 identities and sockets in temporary, isolated profiles."""
import hashlib
import json
import os
import socket
import tempfile
import threading
import time
import unittest
import uuid
from pathlib import Path
from framebridge_expanded import broker, node
class BrokerNodeTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="framebridge-expanded-broker-test-")
self.root = Path(self.temp.name)
self.central = self.root / "central"
self.central.mkdir(mode=0o700)
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
self.port = sock.getsockname()[1]
self.url = "http://127.0.0.1:" + str(self.port)
(self.central / "config.json").write_text(json.dumps({
"schema": 1, "role": "central", "name": "central-test", "node_id": str(uuid.uuid4()),
"ports": {"broker": self.port}, "broker_url": self.url}))
self.server = broker.make_server(port=self.port, state_dir=self.central)
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
self.thread.start()
broker.configure(state_dir=self.central)
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.thread.join(2)
self.temp.cleanup()
def make_node(self, label):
state = self.root / label
state.mkdir(mode=0o700)
(state / "config.json").write_text(json.dumps({
"schema": 1, "role": "node", "name": label, "node_id": str(uuid.uuid4()),
"broker_url": self.url}))
return state
def pair(self, label):
state = self.make_node(label)
public = node.enroll(self.url, label, state)
pending = next(p for p in broker.list_pending() if p["node_id"] == public["node_id"])
response = broker.approve(pending["request_id"], public["node_id"])
self.assertTrue(response["ok"])
paired = node.poll_enrollment(state)
self.assertEqual(paired["state"], "paired")
self.assertFalse(node.status(state)["broker_confirm_required"])
return state, public["node_id"]
def test_http_remote_and_unauthenticated_control_are_rejected(self):
for value in ("http://10.0.0.53:1234", "http://example.com", "https://u:p@example.com", "https://example.com/#x"):
with self.assertRaises(broker.BrokerError):
broker.validate_endpoint(value)
with self.assertRaises(broker.BrokerError):
broker.post_json(self.url + "/fbpair/v1/control/nodes", {}, state_dir=self.central)
def test_approval_requires_exact_uuid_and_operator_auth(self):
state = self.make_node("node-a")
pending = node.enroll(self.url, "node-a", state)
item = broker.list_pending()[0]
self.assertEqual(item["node_id"], pending["node_id"])
self.assertTrue(item["fingerprint"])
self.assertEqual(item["fingerprint"], pending["fingerprint"])
with self.assertRaises(broker.BrokerError):
broker.approve(item["request_id"], str(uuid.uuid4()))
self.assertEqual(broker.list_confirmed_nodes(), [])
def test_two_nodes_identity_confirmation_heartbeat_and_real_read(self):
one, first = self.pair("node-a")
two, second = self.pair("node-b")
self.assertNotEqual(first, second)
before = broker.list_confirmed_nodes()
self.assertEqual(len(before), 2)
self.assertTrue(all(n["ready"] is False for n in before))
for state in (one, two):
self.assertTrue(node.run(state, once=True)["idle"])
self.assertTrue(all(n["ready"] is True for n in broker.list_confirmed_nodes()))
self.assertEqual(node.status(one)["node_id"], first)
stopped = threading.Event()
errors = []
def pump():
while not stopped.is_set():
try:
node.run(one, once=True)
except Exception as exc:
errors.append(type(exc).__name__)
return
stopped.wait(0.05)
worker = threading.Thread(target=pump, daemon=True)
worker.start()
try:
result = broker.request(first, "system.status", {}, timeout_s=5)
self.assertTrue(result["ok"], result)
self.assertIsInstance(result["result"], dict)
self.assertEqual(result["node_id"], first)
finally:
stopped.set()
worker.join(2)
self.assertFalse(errors)
def test_dispatch_rejects_wrong_node_and_bad_digest_before_adapter(self):
state, ident = self.pair("node-a")
called = []
script = "printf harmless"
request = {"request_id": "request-test", "node_id": str(uuid.uuid4()),
"expires_at": time.time() + 30, "operation": "admin.exec", "payload": {
"authorized": True, "job_id": "test-job", "timeout_s": 10,
"script": script, "sha256": hashlib.sha256(script.encode()).hexdigest()}}
self.assertEqual(node.dispatch_request(request, called.append, state)["error"], "REQUEST_NODE_MISMATCH")
request["node_id"] = ident
request["payload"]["sha256"] = "0" * 64
self.assertEqual(node.dispatch_request(request, called.append, state)["error"], "SCRIPT_SHA256_MISMATCH")
self.assertEqual(called, [])
def test_confirmed_identity_survives_broker_restart_but_readiness_does_not(self):
state, ident = self.pair("node-a")
node.run(state, once=True)
self.assertTrue(broker.list_confirmed_nodes()[0]["ready"])
self.server.shutdown()
self.server.server_close()
self.thread.join(2)
self.server = broker.make_server(port=self.port, state_dir=self.central)
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
self.thread.start()
nodes = broker.list_confirmed_nodes()
self.assertEqual(nodes[0]["node_id"], ident)
self.assertFalse(nodes[0]["ready"])
node.run(state, once=True)
self.assertTrue(broker.list_confirmed_nodes()[0]["ready"])
if __name__ == "__main__":
unittest.main()