Initial production analytics foundation
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from urllib.error import HTTPError
|
||||
|
||||
from production_analytics.enlyze.exploration import (
|
||||
AuthenticationError,
|
||||
ExplorationClient,
|
||||
ExplorationSettings,
|
||||
NonJsonResponseError,
|
||||
load_secret_file,
|
||||
)
|
||||
from production_analytics.enlyze.sanitize import sanitize
|
||||
|
||||
|
||||
class _Response:
|
||||
status = 200
|
||||
headers = {"Content-Type": "application/json", "X-Request-Id": "request-123"}
|
||||
|
||||
def __init__(self, body: bytes) -> None:
|
||||
self._body = body
|
||||
|
||||
def read(self) -> bytes:
|
||||
return self._body
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_: object) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class ExplorationClientTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.settings = ExplorationSettings.from_environment(
|
||||
{
|
||||
"ENLYZE_BASE_URL": "https://enlyze.example/",
|
||||
"ENLYZE_API_KEY": "secret",
|
||||
}
|
||||
)
|
||||
|
||||
@patch("production_analytics.enlyze.exploration.urlopen")
|
||||
def test_get_uses_explicit_header_and_decodes_json(self, urlopen_mock: object) -> None:
|
||||
urlopen_mock.return_value = _Response(b'{"items": [1]}') # type: ignore[attr-defined]
|
||||
|
||||
response = ExplorationClient(self.settings).get("/observed/path", {"limit": "1"})
|
||||
|
||||
request = urlopen_mock.call_args.args[0] # type: ignore[attr-defined]
|
||||
self.assertEqual(request.get_method(), "GET")
|
||||
self.assertEqual(request.get_header("Authorization"), "Bearer secret")
|
||||
self.assertEqual(response.body, {"items": [1]})
|
||||
|
||||
@patch("production_analytics.enlyze.exploration.urlopen")
|
||||
def test_non_json_response_is_clear_error(self, urlopen_mock: object) -> None:
|
||||
urlopen_mock.return_value = _Response(b"not json") # type: ignore[attr-defined]
|
||||
|
||||
with self.assertRaises(NonJsonResponseError):
|
||||
ExplorationClient(self.settings).get("/observed/path")
|
||||
|
||||
@patch("production_analytics.enlyze.exploration.urlopen")
|
||||
def test_post_json_sends_json_body_and_bearer_header(self, urlopen_mock: object) -> None:
|
||||
urlopen_mock.return_value = _Response(b'{"data": {}}') # type: ignore[attr-defined]
|
||||
|
||||
ExplorationClient(self.settings).post_json("/v2/timeseries", {"machine": "machine-id"})
|
||||
|
||||
request = urlopen_mock.call_args.args[0] # type: ignore[attr-defined]
|
||||
self.assertEqual(request.get_method(), "POST")
|
||||
self.assertEqual(request.get_header("Content-type"), "application/json")
|
||||
self.assertEqual(request.data, b'{"machine": "machine-id"}')
|
||||
self.assertEqual(request.get_header("Authorization"), "Bearer secret")
|
||||
|
||||
@patch("production_analytics.enlyze.exploration.urlopen")
|
||||
def test_authentication_error_does_not_expose_response_body(self, urlopen_mock: object) -> None:
|
||||
urlopen_mock.side_effect = HTTPError("https://example.invalid", 401, "Unauthorized", {}, io.BytesIO(b"secret")) # type: ignore[attr-defined]
|
||||
|
||||
with self.assertRaisesRegex(AuthenticationError, "HTTP 401"):
|
||||
ExplorationClient(self.settings).get("/observed/path")
|
||||
|
||||
|
||||
class SanitizationTests(unittest.TestCase):
|
||||
def test_sanitization_preserves_shape_and_redacts_sensitive_values(self) -> None:
|
||||
response = sanitize(
|
||||
{
|
||||
"machine_id": "machine-47",
|
||||
"token": "very-secret",
|
||||
"Authorization": "Bearer very-secret",
|
||||
"site_name": "Sensitive Site",
|
||||
"api_url": "https://internal.example/v1/items",
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
"unit": "kW",
|
||||
"value": 12.5,
|
||||
"quality": "good",
|
||||
"items": [{"machine_id": "machine-47"}],
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(response["token"], "<redacted-secret>")
|
||||
self.assertEqual(response["Authorization"], "<redacted-secret>")
|
||||
self.assertEqual(response["site_name"], "<redacted-site_name>")
|
||||
self.assertEqual(response["api_url"], "https://redacted-host.invalid/v1/items")
|
||||
self.assertEqual(response["timestamp"], "2026-01-01T00:00:00Z")
|
||||
self.assertEqual(response["unit"], "kW")
|
||||
self.assertEqual(response["value"], 12.5)
|
||||
self.assertEqual(response["machine_id"], response["items"][0]["machine_id"])
|
||||
|
||||
|
||||
class SecretFileTests(unittest.TestCase):
|
||||
def test_secret_file_is_parsed_without_execution(self) -> None:
|
||||
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8") as secret_file:
|
||||
secret_file.write("ENLYZE_API_KEY='key value'\nUNRELATED=plain # comment\n")
|
||||
secret_file.flush()
|
||||
|
||||
values = load_secret_file(secret_file.name)
|
||||
|
||||
self.assertEqual(values, {"ENLYZE_API_KEY": "key value", "UNRELATED": "plain"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user