From 417a583696deeb6ff78802c490eaa424aeb4d0e8 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Wed, 18 Mar 2026 20:50:56 -0700 Subject: [PATCH] Use proper version formatting. Closes #70. --- app/fanout/community_mqtt.py | 8 ++-- tests/test_community_mqtt.py | 74 ++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/app/fanout/community_mqtt.py b/app/fanout/community_mqtt.py index d726c7f..b3af953 100644 --- a/app/fanout/community_mqtt.py +++ b/app/fanout/community_mqtt.py @@ -115,7 +115,7 @@ def _generate_jwt_token( "exp": now + _TOKEN_LIFETIME, "aud": audience, "owner": pubkey_hex, - "client": _CLIENT_ID, + "client": _get_client_version(), } if email: payload["email"] = email @@ -260,8 +260,10 @@ def _build_radio_info() -> str: def _get_client_version() -> str: - """Return the app version string for community MQTT payloads.""" - return get_app_build_info().version + """Return the canonical client/version identifier for community MQTT.""" + build = get_app_build_info() + commit_hash = build.commit_hash or "unknown" + return f"{_CLIENT_ID}/{build.version}-{commit_hash}" class CommunityMqttPublisher(BaseMqttPublisher): diff --git a/tests/test_community_mqtt.py b/tests/test_community_mqtt.py index b98a60c..5c65c94 100644 --- a/tests/test_community_mqtt.py +++ b/tests/test_community_mqtt.py @@ -108,20 +108,24 @@ class TestJwtGeneration: def test_payload_contains_required_fields(self): private_key, public_key = _make_test_keys() - token = _generate_jwt_token(private_key, public_key) - payload_b64 = token.split(".")[1] - import base64 + with patch( + "app.fanout.community_mqtt.get_app_build_info", + return_value=SimpleNamespace(version="1.2.3", commit_hash="abcdef"), + ): + token = _generate_jwt_token(private_key, public_key) + payload_b64 = token.split(".")[1] + import base64 - padded = payload_b64 + "=" * (4 - len(payload_b64) % 4) - payload = json.loads(base64.urlsafe_b64decode(padded)) - assert payload["publicKey"] == public_key.hex().upper() - assert "iat" in payload - assert "exp" in payload - assert payload["exp"] - payload["iat"] == 86400 - assert payload["aud"] == _DEFAULT_BROKER - assert payload["owner"] == public_key.hex().upper() - assert payload["client"] == _CLIENT_ID - assert "email" not in payload # omitted when empty + padded = payload_b64 + "=" * (4 - len(payload_b64) % 4) + payload = json.loads(base64.urlsafe_b64decode(padded)) + assert payload["publicKey"] == public_key.hex().upper() + assert "iat" in payload + assert "exp" in payload + assert payload["exp"] - payload["iat"] == 86400 + assert payload["aud"] == _DEFAULT_BROKER + assert payload["owner"] == public_key.hex().upper() + assert payload["client"] == f"{_CLIENT_ID}/1.2.3-abcdef" + assert "email" not in payload # omitted when empty def test_payload_includes_email_when_provided(self): private_key, public_key = _make_test_keys() @@ -822,7 +826,10 @@ class TestLwtAndStatusPublish: pub, "_fetch_stats", new_callable=AsyncMock, return_value={"battery_mv": 4200} ), patch("app.fanout.community_mqtt._build_radio_info", return_value="915.0,250.0,10,8"), - patch("app.fanout.community_mqtt._get_client_version", return_value="RemoteTerm 2.4.0"), + patch( + "app.fanout.community_mqtt._get_client_version", + return_value="RemoteTerm/2.4.0-abcdef", + ), patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish, ): await pub._on_connected_async(settings) @@ -842,7 +849,7 @@ class TestLwtAndStatusPublish: assert payload["model"] == "T-Deck" assert payload["firmware_version"] == "v2.2.2 (Build: 2025-01-15)" assert payload["radio"] == "915.0,250.0,10,8" - assert payload["client_version"] == "RemoteTerm 2.4.0" + assert payload["client_version"] == "RemoteTerm/2.4.0-abcdef" assert payload["stats"] == {"battery_mv": 4200} def test_lwt_and_online_share_same_topic(self): @@ -902,7 +909,8 @@ class TestLwtAndStatusPublish: patch.object(pub, "_fetch_stats", new_callable=AsyncMock, return_value=None), patch("app.fanout.community_mqtt._build_radio_info", return_value="0,0,0,0"), patch( - "app.fanout.community_mqtt._get_client_version", return_value="RemoteTerm unknown" + "app.fanout.community_mqtt._get_client_version", + return_value="RemoteTerm/0.0.0-unknown", ), patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish, ): @@ -1215,18 +1223,21 @@ class TestBuildRadioInfo: class TestGetClientVersion: - def test_returns_plain_version(self): - """Should return a bare version string with no product prefix.""" - result = _get_client_version() - assert result - assert "RemoteTerm" not in result - - def test_returns_version_from_build_helper(self): - """Should use the shared backend build-info helper.""" + def test_returns_canonical_client_identifier(self): + """Should return the canonical client/version/hash identifier.""" with patch("app.fanout.community_mqtt.get_app_build_info") as mock_build_info: mock_build_info.return_value.version = "1.2.3" + mock_build_info.return_value.commit_hash = "abcdef" result = _get_client_version() - assert result == "1.2.3" + assert result == "RemoteTerm/1.2.3-abcdef" + + def test_falls_back_to_unknown_hash_when_commit_missing(self): + """Should keep the canonical shape even when the commit hash is unavailable.""" + with patch("app.fanout.community_mqtt.get_app_build_info") as mock_build_info: + mock_build_info.return_value.version = "1.2.3" + mock_build_info.return_value.commit_hash = None + result = _get_client_version() + assert result == "RemoteTerm/1.2.3-unknown" class TestPublishStatus: @@ -1255,7 +1266,10 @@ class TestPublishStatus: ), patch.object(pub, "_fetch_stats", new_callable=AsyncMock, return_value=stats), patch("app.fanout.community_mqtt._build_radio_info", return_value="915.0,250.0,10,8"), - patch("app.fanout.community_mqtt._get_client_version", return_value="RemoteTerm 2.4.0"), + patch( + "app.fanout.community_mqtt._get_client_version", + return_value="RemoteTerm/2.4.0-abcdef", + ), patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish, ): await pub._publish_status(settings) @@ -1268,7 +1282,7 @@ class TestPublishStatus: assert payload["model"] == "T-Deck" assert payload["firmware_version"] == "v2.2.2 (Build: 2025-01-15)" assert payload["radio"] == "915.0,250.0,10,8" - assert payload["client_version"] == "RemoteTerm 2.4.0" + assert payload["client_version"] == "RemoteTerm/2.4.0-abcdef" assert payload["stats"] == stats @pytest.mark.asyncio @@ -1293,7 +1307,8 @@ class TestPublishStatus: patch.object(pub, "_fetch_stats", new_callable=AsyncMock, return_value=None), patch("app.fanout.community_mqtt._build_radio_info", return_value="0,0,0,0"), patch( - "app.fanout.community_mqtt._get_client_version", return_value="RemoteTerm unknown" + "app.fanout.community_mqtt._get_client_version", + return_value="RemoteTerm/0.0.0-unknown", ), patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish, ): @@ -1326,7 +1341,8 @@ class TestPublishStatus: patch.object(pub, "_fetch_stats", new_callable=AsyncMock, return_value=None), patch("app.fanout.community_mqtt._build_radio_info", return_value="0,0,0,0"), patch( - "app.fanout.community_mqtt._get_client_version", return_value="RemoteTerm unknown" + "app.fanout.community_mqtt._get_client_version", + return_value="RemoteTerm/0.0.0-unknown", ), patch.object(pub, "publish", new_callable=AsyncMock), ):