Use proper version formatting. Closes #70.

This commit is contained in:
Jack Kingsman
2026-03-18 20:50:56 -07:00
parent 541dba6a75
commit 417a583696
2 changed files with 50 additions and 32 deletions
+5 -3
View File
@@ -115,7 +115,7 @@ def _generate_jwt_token(
"exp": now + _TOKEN_LIFETIME, "exp": now + _TOKEN_LIFETIME,
"aud": audience, "aud": audience,
"owner": pubkey_hex, "owner": pubkey_hex,
"client": _CLIENT_ID, "client": _get_client_version(),
} }
if email: if email:
payload["email"] = email payload["email"] = email
@@ -260,8 +260,10 @@ def _build_radio_info() -> str:
def _get_client_version() -> str: def _get_client_version() -> str:
"""Return the app version string for community MQTT payloads.""" """Return the canonical client/version identifier for community MQTT."""
return get_app_build_info().version build = get_app_build_info()
commit_hash = build.commit_hash or "unknown"
return f"{_CLIENT_ID}/{build.version}-{commit_hash}"
class CommunityMqttPublisher(BaseMqttPublisher): class CommunityMqttPublisher(BaseMqttPublisher):
+45 -29
View File
@@ -108,20 +108,24 @@ class TestJwtGeneration:
def test_payload_contains_required_fields(self): def test_payload_contains_required_fields(self):
private_key, public_key = _make_test_keys() private_key, public_key = _make_test_keys()
token = _generate_jwt_token(private_key, public_key) with patch(
payload_b64 = token.split(".")[1] "app.fanout.community_mqtt.get_app_build_info",
import base64 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) padded = payload_b64 + "=" * (4 - len(payload_b64) % 4)
payload = json.loads(base64.urlsafe_b64decode(padded)) payload = json.loads(base64.urlsafe_b64decode(padded))
assert payload["publicKey"] == public_key.hex().upper() assert payload["publicKey"] == public_key.hex().upper()
assert "iat" in payload assert "iat" in payload
assert "exp" in payload assert "exp" in payload
assert payload["exp"] - payload["iat"] == 86400 assert payload["exp"] - payload["iat"] == 86400
assert payload["aud"] == _DEFAULT_BROKER assert payload["aud"] == _DEFAULT_BROKER
assert payload["owner"] == public_key.hex().upper() assert payload["owner"] == public_key.hex().upper()
assert payload["client"] == _CLIENT_ID assert payload["client"] == f"{_CLIENT_ID}/1.2.3-abcdef"
assert "email" not in payload # omitted when empty assert "email" not in payload # omitted when empty
def test_payload_includes_email_when_provided(self): def test_payload_includes_email_when_provided(self):
private_key, public_key = _make_test_keys() private_key, public_key = _make_test_keys()
@@ -822,7 +826,10 @@ class TestLwtAndStatusPublish:
pub, "_fetch_stats", new_callable=AsyncMock, return_value={"battery_mv": 4200} 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._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, patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish,
): ):
await pub._on_connected_async(settings) await pub._on_connected_async(settings)
@@ -842,7 +849,7 @@ class TestLwtAndStatusPublish:
assert payload["model"] == "T-Deck" assert payload["model"] == "T-Deck"
assert payload["firmware_version"] == "v2.2.2 (Build: 2025-01-15)" assert payload["firmware_version"] == "v2.2.2 (Build: 2025-01-15)"
assert payload["radio"] == "915.0,250.0,10,8" 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} assert payload["stats"] == {"battery_mv": 4200}
def test_lwt_and_online_share_same_topic(self): 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.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._build_radio_info", return_value="0,0,0,0"),
patch( 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, patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish,
): ):
@@ -1215,18 +1223,21 @@ class TestBuildRadioInfo:
class TestGetClientVersion: class TestGetClientVersion:
def test_returns_plain_version(self): def test_returns_canonical_client_identifier(self):
"""Should return a bare version string with no product prefix.""" """Should return the canonical client/version/hash identifier."""
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."""
with patch("app.fanout.community_mqtt.get_app_build_info") as mock_build_info: 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.version = "1.2.3"
mock_build_info.return_value.commit_hash = "abcdef"
result = _get_client_version() 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: class TestPublishStatus:
@@ -1255,7 +1266,10 @@ class TestPublishStatus:
), ),
patch.object(pub, "_fetch_stats", new_callable=AsyncMock, return_value=stats), 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._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, patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish,
): ):
await pub._publish_status(settings) await pub._publish_status(settings)
@@ -1268,7 +1282,7 @@ class TestPublishStatus:
assert payload["model"] == "T-Deck" assert payload["model"] == "T-Deck"
assert payload["firmware_version"] == "v2.2.2 (Build: 2025-01-15)" assert payload["firmware_version"] == "v2.2.2 (Build: 2025-01-15)"
assert payload["radio"] == "915.0,250.0,10,8" 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 assert payload["stats"] == stats
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -1293,7 +1307,8 @@ class TestPublishStatus:
patch.object(pub, "_fetch_stats", new_callable=AsyncMock, return_value=None), 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._build_radio_info", return_value="0,0,0,0"),
patch( 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, 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.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._build_radio_info", return_value="0,0,0,0"),
patch( 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), patch.object(pub, "publish", new_callable=AsyncMock),
): ):