feat: add telemetry collection functionality
This commit is contained in:
1 parent
5141bcdb0f
commit
5a285ab1a6
10 files changed
+412
-5
No files matched your search
@@ -0,0 +1,100 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import unittest
|
||||
import urllib.error
|
||||
from unittest.mock import patch
|
||||
|
||||
import webssh_backend.telemetry as telemetry
|
||||
from webssh_backend.telemetry import TelemetryClient, canonical_payload, sign_request
|
||||
|
||||
|
||||
class TelemetrySigningTests(unittest.TestCase):
|
||||
def test_canonical_payload_has_exact_newlines_and_body_hash(self):
|
||||
body = b'{"version":"1.0.0"}'
|
||||
expected = "POST\n/api/v1/telemetry/heartbeat\n1700000000\nnonce\nrequest\n" + hashlib.sha256(body).hexdigest()
|
||||
self.assertEqual(canonical_payload("post", "/api/v1/telemetry/heartbeat", 1700000000, "nonce", "request", body), expected)
|
||||
self.assertFalse(expected.endswith("\n"))
|
||||
|
||||
def test_signature_is_lowercase_hmac_sha256(self):
|
||||
body = b"{}"
|
||||
canonical = canonical_payload("POST", "/path", 1, "n", "r", body)
|
||||
expected = hmac.new(b"secret", canonical.encode(), hashlib.sha256).hexdigest()
|
||||
self.assertEqual(sign_request("secret", "POST", "/path", 1, "n", "r", body), expected)
|
||||
self.assertEqual(len(expected), 64)
|
||||
self.assertEqual(expected, expected.lower())
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
status = 201
|
||||
headers = {}
|
||||
|
||||
def __init__(self, payload):
|
||||
self.payload = payload
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
import json
|
||||
return json.dumps(self.payload).encode()
|
||||
|
||||
|
||||
class FakeState:
|
||||
def __init__(self, state=None):
|
||||
self.state = state or {}
|
||||
|
||||
def get_telemetry_state(self):
|
||||
return dict(self.state) if self.state else None
|
||||
|
||||
def save_telemetry_state(self, state):
|
||||
self.state = dict(state)
|
||||
return True
|
||||
|
||||
|
||||
class TelemetryClientTests(unittest.TestCase):
|
||||
def test_disabled_telemetry_does_not_call_opener(self):
|
||||
called = []
|
||||
|
||||
def opener(request, timeout):
|
||||
called.append(request)
|
||||
return FakeResponse({"data": {}})
|
||||
|
||||
with patch.object(telemetry, "creds_manager", FakeState({"credential_secret": "secret"})), patch.object(telemetry, "STRIX_TELEMETRY_ENABLED", False), patch.object(telemetry, "STRIX_TELEMETRY_URL", "https://telemetry.test"):
|
||||
TelemetryClient(opener).uninstall()
|
||||
self.assertEqual(called, [])
|
||||
|
||||
def test_registration_persists_installation_and_credential(self):
|
||||
state = FakeState()
|
||||
opener = lambda request, timeout: FakeResponse({"data": {
|
||||
"installation_uuid": state.state["installation_uuid"],
|
||||
"credential": "credential-id.secret-value",
|
||||
"expires_at": "2099-01-01T00:00:00Z",
|
||||
}})
|
||||
with patch.object(telemetry, "creds_manager", state), patch.object(telemetry, "STRIX_TELEMETRY_URL", "https://telemetry.test"):
|
||||
TelemetryClient(opener).register()
|
||||
self.assertTrue(state.state["installation_uuid"])
|
||||
self.assertEqual(state.state["credential_id"], "credential-id")
|
||||
self.assertEqual(state.state["credential_secret"], "secret-value")
|
||||
|
||||
def test_retry_reuses_request_id_and_regenerates_nonce(self):
|
||||
state = FakeState({
|
||||
"installation_uuid": "installation",
|
||||
"credential_id": "credential-id",
|
||||
"credential_secret": "secret-value",
|
||||
})
|
||||
requests = []
|
||||
|
||||
def opener(request, timeout):
|
||||
requests.append(request)
|
||||
if len(requests) == 1:
|
||||
raise urllib.error.URLError("temporary failure")
|
||||
return FakeResponse({"data": {"accepted": True}})
|
||||
|
||||
with patch.object(telemetry, "creds_manager", state), patch.object(telemetry, "STRIX_TELEMETRY_URL", "https://telemetry.test"), patch.object(telemetry.time, "sleep"):
|
||||
TelemetryClient(opener).heartbeat()
|
||||
self.assertEqual(len(requests), 2)
|
||||
self.assertEqual(requests[0].headers["X-strix-request-id"], requests[1].headers["X-strix-request-id"])
|
||||
self.assertNotEqual(requests[0].headers["X-strix-nonce"], requests[1].headers["X-strix-nonce"])
|
||||
Reference in new issue
Block a user