from __future__ import annotations import io import json import logging from tenbackward.logging_setup import ( JsonFormatter, configure_json_logging, log_error, log_run_summary, log_startup, ) def _capture(logger: logging.Logger) -> io.StringIO: buf = io.StringIO() handler = logging.StreamHandler(buf) handler.setFormatter(JsonFormatter()) logger.handlers = [handler] logger.setLevel(logging.INFO) logger.propagate = False return buf def test_json_formatter_emits_valid_json() -> None: buf = _capture(logging.getLogger("tenbackward.test")) logging.getLogger("tenbackward.test").info("hello", extra={"foo": 1}) line = buf.getvalue().strip() payload = json.loads(line) assert payload["message"] == "hello" assert payload["level"] == "INFO" assert payload["logger"] == "tenbackward.test" assert payload["foo"] == 1 assert "ts" in payload def test_json_formatter_redacts_secret_keys() -> None: buf = _capture(logging.getLogger("tenbackward.test")) logging.getLogger("tenbackward.test").info( "msg", extra={"access_token": "secret-value", "site_url": "https://x"}, ) payload = json.loads(buf.getvalue().strip()) assert payload["access_token"] == "[REDACTED]" assert payload["site_url"] == "https://x" def test_log_run_summary_emits_one_line_with_counters() -> None: buf = _capture(logging.getLogger("tenbackward")) log_run_summary(3, 2, 1, 1, ["x"]) lines = [line for line in buf.getvalue().splitlines() if line.strip()] assert len(lines) == 1 payload = json.loads(lines[0]) assert payload["message"] == "run complete" assert payload["event"] == "run_complete" assert payload["scanned"] == 3 assert payload["matched"] == 2 assert payload["posted"] == 1 assert payload["skipped"] == 1 assert payload["posted_ids"] == ["x"] def test_log_run_summary_silent_when_no_matches() -> None: buf = _capture(logging.getLogger("tenbackward")) log_run_summary(0, 0, 0, 0, []) assert buf.getvalue() == "" def test_log_startup_emits_single_info_line() -> None: buf = _capture(logging.getLogger("tenbackward")) log_startup(version="0.1.0", site_url="https://x", run_at="09:00", tz="Europe/Berlin") payload = json.loads(buf.getvalue().strip()) assert payload["event"] == "startup" assert payload["version"] == "0.1.0" def test_log_error_redacts_secret_keys_and_exposes_exc_type() -> None: buf = _capture(logging.getLogger("tenbackward")) log_error("boom", exc=RuntimeError("x"), access_token="abc") payload = json.loads(buf.getvalue().strip()) assert payload["level"] == "ERROR" assert payload["event"] == "boom" assert payload["access_token"] == "[REDACTED]" assert payload["exc_type"] == "RuntimeError" def test_configure_json_logging_is_idempotent() -> None: configure_json_logging() configure_json_logging() root = logging.getLogger() json_handlers = [h for h in root.handlers if isinstance(h.formatter, JsonFormatter)] assert len(json_handlers) == 1