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"], "") self.assertEqual(response["Authorization"], "") self.assertEqual(response["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()