diff --git a/app/routers/fanout.py b/app/routers/fanout.py index 676f52b1..36c7d400 100644 --- a/app/routers/fanout.py +++ b/app/routers/fanout.py @@ -78,6 +78,26 @@ class FanoutConfigUpdate(BaseModel): enabled: bool | None = Field(default=None, description="Enable/disable toggle") +def _validate_and_normalize_config(config_type: str, config: dict) -> dict: + """Validate a config blob and return the canonical persisted form.""" + normalized = dict(config) + + if config_type == "mqtt_private": + _validate_mqtt_private_config(normalized) + elif config_type == "mqtt_community": + _validate_mqtt_community_config(normalized) + elif config_type == "bot": + _validate_bot_config(normalized) + elif config_type == "webhook": + _validate_webhook_config(normalized) + elif config_type == "apprise": + _validate_apprise_config(normalized) + elif config_type == "sqs": + _validate_sqs_config(normalized) + + return normalized + + def _validate_mqtt_private_config(config: dict) -> None: """Validate mqtt_private config blob.""" if not config.get("broker_host"): @@ -323,28 +343,13 @@ async def create_fanout_config(body: FanoutConfigCreate) -> dict: if body.type == "bot" and server_settings.disable_bots: raise HTTPException(status_code=403, detail="Bot system disabled by server configuration") - # Only validate config when creating as enabled — disabled configs - # are drafts the user hasn't finished configuring yet. - if body.enabled: - if body.type == "mqtt_private": - _validate_mqtt_private_config(body.config) - elif body.type == "mqtt_community": - _validate_mqtt_community_config(body.config) - elif body.type == "bot": - _validate_bot_config(body.config) - elif body.type == "webhook": - _validate_webhook_config(body.config) - elif body.type == "apprise": - _validate_apprise_config(body.config) - elif body.type == "sqs": - _validate_sqs_config(body.config) - + normalized_config = _validate_and_normalize_config(body.type, body.config) scope = _enforce_scope(body.type, body.scope) cfg = await FanoutConfigRepository.create( config_type=body.type, name=body.name, - config=body.config, + config=normalized_config, scope=scope, enabled=body.enabled, ) @@ -374,27 +379,11 @@ async def update_fanout_config(config_id: str, body: FanoutConfigUpdate) -> dict kwargs["name"] = body.name if body.enabled is not None: kwargs["enabled"] = body.enabled - if body.config is not None: - kwargs["config"] = body.config if body.scope is not None: kwargs["scope"] = _enforce_scope(existing["type"], body.scope) - # Validate config when the result will be enabled - will_be_enabled = body.enabled if body.enabled is not None else existing["enabled"] - if will_be_enabled: - config_to_validate = body.config if body.config is not None else existing["config"] - if existing["type"] == "mqtt_private": - _validate_mqtt_private_config(config_to_validate) - elif existing["type"] == "mqtt_community": - _validate_mqtt_community_config(config_to_validate) - elif existing["type"] == "bot": - _validate_bot_config(config_to_validate) - elif existing["type"] == "webhook": - _validate_webhook_config(config_to_validate) - elif existing["type"] == "apprise": - _validate_apprise_config(config_to_validate) - elif existing["type"] == "sqs": - _validate_sqs_config(config_to_validate) + config_to_validate = body.config if body.config is not None else existing["config"] + kwargs["config"] = _validate_and_normalize_config(existing["type"], config_to_validate) updated = await FanoutConfigRepository.update(config_id, **kwargs) if updated is None: diff --git a/frontend/prebuilt/assets/BotCodeEditor-C20Gx7-Q.js b/frontend/prebuilt/assets/BotCodeEditor-D8HqJbrh.js similarity index 99% rename from frontend/prebuilt/assets/BotCodeEditor-C20Gx7-Q.js rename to frontend/prebuilt/assets/BotCodeEditor-D8HqJbrh.js index ccf03948..7e9d5f42 100644 Binary files a/frontend/prebuilt/assets/BotCodeEditor-C20Gx7-Q.js and b/frontend/prebuilt/assets/BotCodeEditor-D8HqJbrh.js differ diff --git a/frontend/prebuilt/assets/BotCodeEditor-C20Gx7-Q.js.map b/frontend/prebuilt/assets/BotCodeEditor-D8HqJbrh.js.map similarity index 99% rename from frontend/prebuilt/assets/BotCodeEditor-C20Gx7-Q.js.map rename to frontend/prebuilt/assets/BotCodeEditor-D8HqJbrh.js.map index ec556d0f..09e4bc81 100644 Binary files a/frontend/prebuilt/assets/BotCodeEditor-C20Gx7-Q.js.map and b/frontend/prebuilt/assets/BotCodeEditor-D8HqJbrh.js.map differ diff --git a/frontend/prebuilt/assets/CrackerPanel-D45MeFE8.js b/frontend/prebuilt/assets/CrackerPanel-CQP9hluh.js similarity index 99% rename from frontend/prebuilt/assets/CrackerPanel-D45MeFE8.js rename to frontend/prebuilt/assets/CrackerPanel-CQP9hluh.js index 7666fea3..6a72552b 100644 Binary files a/frontend/prebuilt/assets/CrackerPanel-D45MeFE8.js and b/frontend/prebuilt/assets/CrackerPanel-CQP9hluh.js differ diff --git a/frontend/prebuilt/assets/CrackerPanel-D45MeFE8.js.map b/frontend/prebuilt/assets/CrackerPanel-CQP9hluh.js.map similarity index 99% rename from frontend/prebuilt/assets/CrackerPanel-D45MeFE8.js.map rename to frontend/prebuilt/assets/CrackerPanel-CQP9hluh.js.map index 6dcc4fc3..0e66eb55 100644 Binary files a/frontend/prebuilt/assets/CrackerPanel-D45MeFE8.js.map and b/frontend/prebuilt/assets/CrackerPanel-CQP9hluh.js.map differ diff --git a/frontend/prebuilt/assets/MapView-D0gphXtL.js b/frontend/prebuilt/assets/MapView-ClQphI6P.js similarity index 92% rename from frontend/prebuilt/assets/MapView-D0gphXtL.js rename to frontend/prebuilt/assets/MapView-ClQphI6P.js index f8256f16..476282a0 100644 Binary files a/frontend/prebuilt/assets/MapView-D0gphXtL.js and b/frontend/prebuilt/assets/MapView-ClQphI6P.js differ diff --git a/frontend/prebuilt/assets/MapView-D0gphXtL.js.map b/frontend/prebuilt/assets/MapView-ClQphI6P.js.map similarity index 99% rename from frontend/prebuilt/assets/MapView-D0gphXtL.js.map rename to frontend/prebuilt/assets/MapView-ClQphI6P.js.map index b51ff89c..2c79d604 100644 Binary files a/frontend/prebuilt/assets/MapView-D0gphXtL.js.map and b/frontend/prebuilt/assets/MapView-ClQphI6P.js.map differ diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-zqanKONo.js b/frontend/prebuilt/assets/NeighborsMiniMap-Bi17OsoB.js similarity index 85% rename from frontend/prebuilt/assets/NeighborsMiniMap-zqanKONo.js rename to frontend/prebuilt/assets/NeighborsMiniMap-Bi17OsoB.js index be611bc7..c1e901fb 100644 Binary files a/frontend/prebuilt/assets/NeighborsMiniMap-zqanKONo.js and b/frontend/prebuilt/assets/NeighborsMiniMap-Bi17OsoB.js differ diff --git a/frontend/prebuilt/assets/NeighborsMiniMap-zqanKONo.js.map b/frontend/prebuilt/assets/NeighborsMiniMap-Bi17OsoB.js.map similarity index 97% rename from frontend/prebuilt/assets/NeighborsMiniMap-zqanKONo.js.map rename to frontend/prebuilt/assets/NeighborsMiniMap-Bi17OsoB.js.map index c98d37b8..00f04310 100644 Binary files a/frontend/prebuilt/assets/NeighborsMiniMap-zqanKONo.js.map and b/frontend/prebuilt/assets/NeighborsMiniMap-Bi17OsoB.js.map differ diff --git a/frontend/prebuilt/assets/PathRouteMap-kT7xfoMb.js b/frontend/prebuilt/assets/PathRouteMap-C6PZ0wFI.js similarity index 95% rename from frontend/prebuilt/assets/PathRouteMap-kT7xfoMb.js rename to frontend/prebuilt/assets/PathRouteMap-C6PZ0wFI.js index d4189b48..7f227ecc 100644 Binary files a/frontend/prebuilt/assets/PathRouteMap-kT7xfoMb.js and b/frontend/prebuilt/assets/PathRouteMap-C6PZ0wFI.js differ diff --git a/frontend/prebuilt/assets/PathRouteMap-kT7xfoMb.js.map b/frontend/prebuilt/assets/PathRouteMap-C6PZ0wFI.js.map similarity index 99% rename from frontend/prebuilt/assets/PathRouteMap-kT7xfoMb.js.map rename to frontend/prebuilt/assets/PathRouteMap-C6PZ0wFI.js.map index c2c52770..acb0c693 100644 Binary files a/frontend/prebuilt/assets/PathRouteMap-kT7xfoMb.js.map and b/frontend/prebuilt/assets/PathRouteMap-C6PZ0wFI.js.map differ diff --git a/frontend/prebuilt/assets/Popup-C-Q9ygBf.js b/frontend/prebuilt/assets/Popup-A7yuH1dp.js similarity index 81% rename from frontend/prebuilt/assets/Popup-C-Q9ygBf.js rename to frontend/prebuilt/assets/Popup-A7yuH1dp.js index a6b954ed..b4fea0e3 100644 Binary files a/frontend/prebuilt/assets/Popup-C-Q9ygBf.js and b/frontend/prebuilt/assets/Popup-A7yuH1dp.js differ diff --git a/frontend/prebuilt/assets/Popup-C-Q9ygBf.js.map b/frontend/prebuilt/assets/Popup-A7yuH1dp.js.map similarity index 97% rename from frontend/prebuilt/assets/Popup-C-Q9ygBf.js.map rename to frontend/prebuilt/assets/Popup-A7yuH1dp.js.map index 5f4d19a0..077cc1fc 100644 Binary files a/frontend/prebuilt/assets/Popup-C-Q9ygBf.js.map and b/frontend/prebuilt/assets/Popup-A7yuH1dp.js.map differ diff --git a/frontend/prebuilt/assets/RepeaterDashboard-DtQKwbsT.js b/frontend/prebuilt/assets/RepeaterDashboard-Dj09PNsp.js similarity index 98% rename from frontend/prebuilt/assets/RepeaterDashboard-DtQKwbsT.js rename to frontend/prebuilt/assets/RepeaterDashboard-Dj09PNsp.js index 0bed4bde..173f1bf8 100644 Binary files a/frontend/prebuilt/assets/RepeaterDashboard-DtQKwbsT.js and b/frontend/prebuilt/assets/RepeaterDashboard-Dj09PNsp.js differ diff --git a/frontend/prebuilt/assets/RepeaterDashboard-DtQKwbsT.js.map b/frontend/prebuilt/assets/RepeaterDashboard-Dj09PNsp.js.map similarity index 99% rename from frontend/prebuilt/assets/RepeaterDashboard-DtQKwbsT.js.map rename to frontend/prebuilt/assets/RepeaterDashboard-Dj09PNsp.js.map index 3208dbf4..7c9e703f 100644 Binary files a/frontend/prebuilt/assets/RepeaterDashboard-DtQKwbsT.js.map and b/frontend/prebuilt/assets/RepeaterDashboard-Dj09PNsp.js.map differ diff --git a/frontend/prebuilt/assets/SearchView-DBqP6hD6.js b/frontend/prebuilt/assets/SearchView-Dcvv4c8P.js similarity index 97% rename from frontend/prebuilt/assets/SearchView-DBqP6hD6.js rename to frontend/prebuilt/assets/SearchView-Dcvv4c8P.js index f28b3310..8d83519c 100644 Binary files a/frontend/prebuilt/assets/SearchView-DBqP6hD6.js and b/frontend/prebuilt/assets/SearchView-Dcvv4c8P.js differ diff --git a/frontend/prebuilt/assets/SearchView-DBqP6hD6.js.map b/frontend/prebuilt/assets/SearchView-Dcvv4c8P.js.map similarity index 99% rename from frontend/prebuilt/assets/SearchView-DBqP6hD6.js.map rename to frontend/prebuilt/assets/SearchView-Dcvv4c8P.js.map index 126eb768..fdda9aae 100644 Binary files a/frontend/prebuilt/assets/SearchView-DBqP6hD6.js.map and b/frontend/prebuilt/assets/SearchView-Dcvv4c8P.js.map differ diff --git a/frontend/prebuilt/assets/SettingsModal-BPGiDC12.js b/frontend/prebuilt/assets/SettingsModal-kexfpcq5.js similarity index 99% rename from frontend/prebuilt/assets/SettingsModal-BPGiDC12.js rename to frontend/prebuilt/assets/SettingsModal-kexfpcq5.js index 6b64f277..516f998f 100644 Binary files a/frontend/prebuilt/assets/SettingsModal-BPGiDC12.js and b/frontend/prebuilt/assets/SettingsModal-kexfpcq5.js differ diff --git a/frontend/prebuilt/assets/SettingsModal-BPGiDC12.js.map b/frontend/prebuilt/assets/SettingsModal-kexfpcq5.js.map similarity index 99% rename from frontend/prebuilt/assets/SettingsModal-BPGiDC12.js.map rename to frontend/prebuilt/assets/SettingsModal-kexfpcq5.js.map index 9ae08a54..90770876 100644 Binary files a/frontend/prebuilt/assets/SettingsModal-BPGiDC12.js.map and b/frontend/prebuilt/assets/SettingsModal-kexfpcq5.js.map differ diff --git a/frontend/prebuilt/assets/VisualizerView-Cv9KCsSr.js b/frontend/prebuilt/assets/VisualizerView-BLZNJTl2.js similarity index 99% rename from frontend/prebuilt/assets/VisualizerView-Cv9KCsSr.js rename to frontend/prebuilt/assets/VisualizerView-BLZNJTl2.js index 92a61cf1..eee90286 100644 Binary files a/frontend/prebuilt/assets/VisualizerView-Cv9KCsSr.js and b/frontend/prebuilt/assets/VisualizerView-BLZNJTl2.js differ diff --git a/frontend/prebuilt/assets/VisualizerView-Cv9KCsSr.js.map b/frontend/prebuilt/assets/VisualizerView-BLZNJTl2.js.map similarity index 99% rename from frontend/prebuilt/assets/VisualizerView-Cv9KCsSr.js.map rename to frontend/prebuilt/assets/VisualizerView-BLZNJTl2.js.map index 1fa57bf5..11853c7f 100644 Binary files a/frontend/prebuilt/assets/VisualizerView-Cv9KCsSr.js.map and b/frontend/prebuilt/assets/VisualizerView-BLZNJTl2.js.map differ diff --git a/frontend/prebuilt/assets/hooks-BwtZ3ZXP.js b/frontend/prebuilt/assets/hooks-BwtZ3ZXP.js new file mode 100644 index 00000000..594681c9 Binary files /dev/null and b/frontend/prebuilt/assets/hooks-BwtZ3ZXP.js differ diff --git a/frontend/prebuilt/assets/hooks-LL30L9PN.js.map b/frontend/prebuilt/assets/hooks-BwtZ3ZXP.js.map similarity index 90% rename from frontend/prebuilt/assets/hooks-LL30L9PN.js.map rename to frontend/prebuilt/assets/hooks-BwtZ3ZXP.js.map index 91ed0a43..727a5664 100644 Binary files a/frontend/prebuilt/assets/hooks-LL30L9PN.js.map and b/frontend/prebuilt/assets/hooks-BwtZ3ZXP.js.map differ diff --git a/frontend/prebuilt/assets/hooks-LL30L9PN.js b/frontend/prebuilt/assets/hooks-LL30L9PN.js deleted file mode 100644 index 7860626a..00000000 Binary files a/frontend/prebuilt/assets/hooks-LL30L9PN.js and /dev/null differ diff --git a/frontend/prebuilt/assets/index-Fnq3QmNw.js b/frontend/prebuilt/assets/index-CyRLXsJ_.js similarity index 99% rename from frontend/prebuilt/assets/index-Fnq3QmNw.js rename to frontend/prebuilt/assets/index-CyRLXsJ_.js index 83ebf8ea..00abf741 100644 Binary files a/frontend/prebuilt/assets/index-Fnq3QmNw.js and b/frontend/prebuilt/assets/index-CyRLXsJ_.js differ diff --git a/frontend/prebuilt/assets/index-Fnq3QmNw.js.map b/frontend/prebuilt/assets/index-CyRLXsJ_.js.map similarity index 99% rename from frontend/prebuilt/assets/index-Fnq3QmNw.js.map rename to frontend/prebuilt/assets/index-CyRLXsJ_.js.map index 8b8f914c..b4040969 100644 Binary files a/frontend/prebuilt/assets/index-Fnq3QmNw.js.map and b/frontend/prebuilt/assets/index-CyRLXsJ_.js.map differ diff --git a/frontend/prebuilt/assets/leaflet-ENrCR1B4.js b/frontend/prebuilt/assets/leaflet-IXizDMbG.js similarity index 99% rename from frontend/prebuilt/assets/leaflet-ENrCR1B4.js rename to frontend/prebuilt/assets/leaflet-IXizDMbG.js index e2a609dd..a1073b13 100644 Binary files a/frontend/prebuilt/assets/leaflet-ENrCR1B4.js and b/frontend/prebuilt/assets/leaflet-IXizDMbG.js differ diff --git a/frontend/prebuilt/assets/leaflet-ENrCR1B4.js.map b/frontend/prebuilt/assets/leaflet-IXizDMbG.js.map similarity index 99% rename from frontend/prebuilt/assets/leaflet-ENrCR1B4.js.map rename to frontend/prebuilt/assets/leaflet-IXizDMbG.js.map index e9e2b2a6..909995de 100644 Binary files a/frontend/prebuilt/assets/leaflet-ENrCR1B4.js.map and b/frontend/prebuilt/assets/leaflet-IXizDMbG.js.map differ diff --git a/frontend/prebuilt/assets/separator-BwjFWJb_.js b/frontend/prebuilt/assets/separator-CrdYjM_C.js similarity index 89% rename from frontend/prebuilt/assets/separator-BwjFWJb_.js rename to frontend/prebuilt/assets/separator-CrdYjM_C.js index f6c43124..214a5d8d 100644 Binary files a/frontend/prebuilt/assets/separator-BwjFWJb_.js and b/frontend/prebuilt/assets/separator-CrdYjM_C.js differ diff --git a/frontend/prebuilt/assets/separator-BwjFWJb_.js.map b/frontend/prebuilt/assets/separator-CrdYjM_C.js.map similarity index 98% rename from frontend/prebuilt/assets/separator-BwjFWJb_.js.map rename to frontend/prebuilt/assets/separator-CrdYjM_C.js.map index 8ebcd5d6..3b0dc284 100644 Binary files a/frontend/prebuilt/assets/separator-BwjFWJb_.js.map and b/frontend/prebuilt/assets/separator-CrdYjM_C.js.map differ diff --git a/frontend/prebuilt/index.html b/frontend/prebuilt/index.html index 4ca2a682..42f36104 100644 Binary files a/frontend/prebuilt/index.html and b/frontend/prebuilt/index.html differ diff --git a/tests/test_fanout_hitlist.py b/tests/test_fanout_hitlist.py index fe60090f..e7eec4d2 100644 --- a/tests/test_fanout_hitlist.py +++ b/tests/test_fanout_hitlist.py @@ -728,3 +728,54 @@ class TestCommunityMqttIataValidation: _validate_mqtt_community_config({"iata": "PDX", "auth_mode": "password"}) assert exc_info.value.status_code == 400 assert "username and password" in exc_info.value.detail + + +class TestFanoutConfigMutationInvariant: + """Persisted fanout rows should always be valid and canonical.""" + + @pytest.mark.asyncio + async def test_disabled_create_still_validates_config(self, test_db): + from app.routers.fanout import FanoutConfigCreate, create_fanout_config + + with pytest.raises(HTTPException) as exc_info: + await create_fanout_config( + FanoutConfigCreate( + type="mqtt_community", + name="Invalid draft", + config={}, + scope={}, + enabled=False, + ) + ) + + assert exc_info.value.status_code == 400 + assert "IATA" in exc_info.value.detail + + @pytest.mark.asyncio + async def test_enable_only_patch_persists_normalized_config(self, test_db): + from app.repository.fanout import FanoutConfigRepository + from app.routers.fanout import FanoutConfigUpdate, update_fanout_config + + cfg = await FanoutConfigRepository.create( + config_type="mqtt_community", + name="Community MQTT", + config={ + "iata": "PDX", + "broker_host": " mqtt.example.com ", + "topic_template": "mesh2mqtt/{iata}/node/{Public_Key}", + }, + scope={"messages": "none", "raw_packets": "all"}, + enabled=False, + ) + + with patch("app.fanout.manager.fanout_manager.reload_config", new_callable=AsyncMock): + updated = await update_fanout_config( + cfg["id"], + FanoutConfigUpdate(enabled=True), + ) + + assert updated["enabled"] is True + assert updated["config"]["broker_host"] == "mqtt.example.com" + assert updated["config"]["topic_template"] == "mesh2mqtt/{IATA}/node/{PUBLIC_KEY}" + assert updated["config"]["transport"] == "websockets" + assert updated["config"]["auth_mode"] == "token"