import io import json import os import tempfile import unittest from contextlib import redirect_stderr, redirect_stdout from pathlib import Path from unittest.mock import patch from production_analytics.cli.__main__ import main from production_analytics.enlyze.exploration import ExplorationResponse class CliTests(unittest.TestCase): def test_power_meter_config_error_returns_controlled_exit_code(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: missing_config = Path(temporary_directory) / "missing.yaml" error_output = io.StringIO() with redirect_stderr(error_output): result = main([ "run", "power-meter", "--config", str(missing_config), "--meter", "B2", "--start", "2026-10-01T10:00:00Z", "--end", "2026-10-01T10:10:00Z", ]) self.assertEqual(result, 2) self.assertIn( "Config/startup error: Cannot read power-meter configuration", error_output.getvalue() ) @patch.dict(os.environ, {"ENLYZE_BASE_URL": "https://enlyze.example"}, clear=True) @patch("production_analytics.cli.__main__.ExplorationClient.get") def test_raw_prints_and_saves_sanitized_response(self, get_mock: object) -> None: get_mock.return_value = ExplorationResponse( # type: ignore[attr-defined] status_code=200, path="/verified/path", headers={"Content-Type": "application/json"}, body={"api_token": "secret", "authorization": "Bearer secret", "value": 4.2}, ) with tempfile.TemporaryDirectory() as temporary_directory: output = io.StringIO() with redirect_stdout(output): result = main( [ "enlyze", "raw", "/verified/path", "--save-fixture", "example.json", "--fixture-dir", temporary_directory, "--save-raw", "raw-example.json", "--raw-dir", temporary_directory, ] ) fixture = json.loads((Path(temporary_directory) / "example.json").read_text()) raw_capture = json.loads((Path(temporary_directory) / "raw-example.json").read_text()) self.assertEqual(result, 0) self.assertEqual(fixture["response"]["body"]["api_token"], "") self.assertEqual(fixture["response"]["body"]["authorization"], "") self.assertNotIn("Bearer secret", output.getvalue()) self.assertEqual(raw_capture["response"]["body"]["api_token"], "secret") def test_raw_rejects_unsafe_fixture_name(self) -> None: with patch.dict(os.environ, {"ENLYZE_BASE_URL": "https://enlyze.example"}, clear=True): with patch("production_analytics.cli.__main__.ExplorationClient.get") as get_mock: get_mock.return_value = ExplorationResponse(200, "/path", {}, {}) output = io.StringIO() with redirect_stdout(output): result = main(["enlyze", "raw", "/path", "--save-fixture", "../unsafe.json"]) self.assertEqual(result, 1) self.assertIn("plain filename", output.getvalue()) @patch.dict(os.environ, {"ENLYZE_BASE_URL": "https://enlyze.example"}, clear=True) @patch("production_analytics.cli.__main__.ExplorationClient.post_json") def test_timeseries_builds_schema_supported_read_only_post(self, post_mock: object) -> None: post_mock.return_value = ExplorationResponse( # type: ignore[attr-defined] 200, "/v2/timeseries", {}, {"metadata": {"next_cursor": None}, "data": {"columns": [], "records": []}} ) with tempfile.TemporaryDirectory() as temporary_directory: result = main( [ "enlyze", "timeseries", "--machine", "machine-id", "--start", "2026-01-01T00:00:00+00:00", "--end", "2026-01-01T00:10:00+00:00", "--variable", "variable-id", "--resampling-interval", "10", "--resampling-method", "avg", "--save-fixture", "timeseries.json", "--fixture-dir", temporary_directory, ] ) fixture = json.loads((Path(temporary_directory) / "timeseries.json").read_text()) self.assertEqual(result, 0) self.assertEqual(post_mock.call_args.args[0], "/v2/timeseries") # type: ignore[attr-defined] self.assertEqual(post_mock.call_args.args[1]["resampling_interval"], 10) # type: ignore[attr-defined] self.assertEqual(fixture["request"]["method"], "POST")