122 lines
4.7 KiB
Python
122 lines
4.7 KiB
Python
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()
|