import json import tempfile import time import unittest from pathlib import Path, PurePosixPath from archive_clients.config import ServiceConfig from archive_clients.syncthing import ( RoutePathConflict, RouteSetupTimeout, SyncthingRouteManager, ) from archive_control.v1 import route_pb2 class FakeTransport: def __init__(self): self.status = {"myID": "LOCAL"} self.config = {"devices": [], "folders": []} self.puts = [] self.posts = [] def get_json(self, path): if path == "/rest/system/status": return self.status if path == "/rest/config": return self.config raise AssertionError(path) def put_json(self, path, payload): self.puts.append((path, payload)) if path.startswith("/rest/config/devices/"): self.config["devices"].append(payload) elif path.startswith("/rest/config/folders/"): self.config["folders"].append(payload) else: raise AssertionError(path) def post(self, path): self.posts.append(path) class SyncthingRouteManagerTests(unittest.TestCase): def setUp(self): self.temp_dir = tempfile.TemporaryDirectory() self.root = Path(self.temp_dir.name) / "sync" self.root.mkdir() self.transport = FakeTransport() self.manager = SyncthingRouteManager( ServiceConfig( "http://syncthing", PurePosixPath("/sync"), self.root, ), sparse_supported=True, transport=self.transport, poll_interval=0, ) self.spec = route_pb2.EnsureRouteSpec( route_id="route-1", peer_client_id="archive-1", peer_syncthing_device_id="PEER", peer_addresses=["tcp://archive:22000"], local_relative_path="routes/route-1", setup_timeout_seconds=1800, ) def tearDown(self): self.temp_dir.cleanup() def test_configure_adds_only_peer_and_pairwise_folder(self): configured = self.manager.configure( self.spec, time.monotonic() + 1 ) self.assertEqual(configured.local_device_id, "LOCAL") self.assertTrue(configured.local_path.is_dir()) self.assertEqual(len(self.transport.puts), 2) folder = self.transport.config["folders"][0] self.assertEqual(folder["path"], "/sync/routes/route-1") self.assertEqual(folder["type"], "sendreceive") self.assertEqual( {item["deviceID"] for item in folder["devices"]}, {"LOCAL", "PEER"}, ) self.assertTrue(configured.local_route.archive_control_created) repeated = self.manager.configure(self.spec, time.monotonic() + 1) self.assertEqual(len(self.transport.puts), 2) self.assertFalse(repeated.local_route.archive_control_created) def test_existing_folder_conflicts_are_never_overwritten(self): self.transport.config["folders"].append( { "id": "route-1", "path": "/somewhere-else", "type": "sendreceive", "devices": [{"deviceID": "LOCAL"}, {"deviceID": "PEER"}], } ) with self.assertRaisesRegex(RoutePathConflict, "different path"): self.manager.configure(self.spec, time.monotonic() + 1) self.assertEqual(self.transport.puts, []) def test_bidirectional_nonce_and_ack_are_required(self): configured = self.manager.configure(self.spec, time.monotonic() + 1) peer_nonce = configured.local_path / ( ".archive-control-route-nonce.archive-1" ) peer_nonce.write_text( json.dumps({ "route_id": "route-1", "client_id": "archive-1", "nonce": "peer-nonce", }), encoding="utf-8", ) acknowledgement = configured.local_path / ( ".archive-control-route-ack.cache-1.archive-1" ) acknowledgement.write_text( json.dumps({"route_id": "route-1", "nonce": "local-nonce"}), encoding="utf-8", ) verified = self.manager.verify( configured, self.spec, "cache-1", "local-nonce", time.monotonic() + 1, ) self.assertEqual(verified, (True, True)) peer_ack = configured.local_path / ( ".archive-control-route-ack.archive-1.cache-1" ) self.assertEqual(json.loads(peer_ack.read_text())["nonce"], "peer-nonce") def test_verification_times_out_without_peer_evidence(self): configured = self.manager.configure(self.spec, time.monotonic() + 1) with self.assertRaises(RouteSetupTimeout): self.manager.verify( configured, self.spec, "cache-1", "local-nonce", time.monotonic() + 0.01, ) self.assertTrue(self.transport.posts) if __name__ == "__main__": unittest.main()