diff --git a/backends/advanced/src/advanced_omi_backend/models/vault_sync.py b/backends/advanced/src/advanced_omi_backend/models/vault_sync.py new file mode 100644 index 00000000..0e580d34 --- /dev/null +++ b/backends/advanced/src/advanced_omi_backend/models/vault_sync.py @@ -0,0 +1,12 @@ +"""Request models for the vault sync pairing broker.""" + +from pydantic import BaseModel, Field + +# Syncthing's canonical device ID is eight groups of seven base32 characters. +# Enforce that shape before the ID is interpolated into Syncthing REST paths. +_DEVICE_ID_PATTERN = r"^[A-Z2-7]{7}(?:-[A-Z2-7]{7}){7}$" + + +class PairRequest(BaseModel): + device_id: str = Field(pattern=_DEVICE_ID_PATTERN) + device_name: str = "mac" diff --git a/backends/advanced/src/advanced_omi_backend/routers/modules/vault_sync_routes.py b/backends/advanced/src/advanced_omi_backend/routers/modules/vault_sync_routes.py index 9c960ae0..16f7867c 100644 --- a/backends/advanced/src/advanced_omi_backend/routers/modules/vault_sync_routes.py +++ b/backends/advanced/src/advanced_omi_backend/routers/modules/vault_sync_routes.py @@ -19,9 +19,9 @@ import httpx from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel from advanced_omi_backend.auth import current_active_user +from advanced_omi_backend.models.vault_sync import PairRequest from advanced_omi_backend.users import User logger = logging.getLogger(__name__) @@ -68,11 +68,6 @@ async def _server_device_id(client: httpx.AsyncClient) -> str: return resp.json()["myID"] -class PairRequest(BaseModel): - device_id: str - device_name: str = "mac" - - @router.get("/info") async def vault_sync_info(current_user: User = Depends(current_active_user)): """Return what the Mac needs to dial and identify the server's vault folder.""" diff --git a/backends/advanced/tests/test_vault_sync_routes.py b/backends/advanced/tests/test_vault_sync_routes.py new file mode 100644 index 00000000..8154589f --- /dev/null +++ b/backends/advanced/tests/test_vault_sync_routes.py @@ -0,0 +1,26 @@ +import pytest +from pydantic import ValidationError + +from advanced_omi_backend.models.vault_sync import PairRequest + +VALID_DEVICE_ID = "S7UKX27-GI7ZTXS-GC6RKUA-7AJGZ44-C6NAYEB-HSKTJQK-KJHU2NO-CWV7EQW" + + +def test_pair_request_accepts_canonical_syncthing_device_id(): + request = PairRequest(device_id=VALID_DEVICE_ID) + + assert request.device_id == VALID_DEVICE_ID + + +@pytest.mark.parametrize( + "device_id", + [ + "../../rest/system/shutdown", + f"{VALID_DEVICE_ID}?ignored=true", + VALID_DEVICE_ID.replace("-", ""), + "not-a-device-id", + ], +) +def test_pair_request_rejects_noncanonical_device_id(device_id): + with pytest.raises(ValidationError, match="device_id"): + PairRequest(device_id=device_id)