afv-library/skills/agentforce-d360-analyze/scripts/tests/test_fetch_dc_helpers.py

267 lines
11 KiB
Python

"""Tests for ``fetch_dc`` helpers + DC-access-denied flow.
Complements ``test_fetch_dc_identity.py`` (which targets ``_resolve_identity``
specifically). This file lifts coverage of the other public helpers:
- ``DcAccessDenied`` exception carrier
- ``preflight_dc_access`` 401/403 classification + happy path
- ``_emit_dc_access_denied_preamble`` headless JSON shape
- ``_handle_dc_access_denied`` interactive (tty) vs headless branches
- ``_in_list`` SQL IN-fragment helper (dedup + NOT_SET filter)
- ``_extract_trace_ids`` runtime trace-id extraction (DMO + HTML-escaped)
- ``_preflight_templates`` SQL template existence check
All subprocess + DC-access boundaries are mocked.
"""
from __future__ import annotations
import io
import json
import unittest
from unittest import mock
from . import _bootstrap # noqa: F401 — sys.path setup
import fetch_dc # type: ignore
from dc import DCQueryError # type: ignore
# -----------------------------------------------------------------------------
# DcAccessDenied — small carrier
# -----------------------------------------------------------------------------
class DcAccessDeniedTests(unittest.TestCase):
def test_str_includes_reason_and_detail(self):
e = fetch_dc.DcAccessDenied("401", "Unauthorized")
self.assertEqual(e.reason, "401")
self.assertEqual(e.detail, "Unauthorized")
self.assertIn("401: Unauthorized", str(e))
# -----------------------------------------------------------------------------
# preflight_dc_access — classify HTTP error code
# -----------------------------------------------------------------------------
class PreflightDcAccessTests(unittest.TestCase):
def test_returns_url_and_token_on_success(self):
with mock.patch.object(
fetch_dc, "resolve_org",
return_value=("https://example.salesforce.com", "TOKEN"),
):
with mock.patch.object(fetch_dc, "post", return_value=[]):
url, token = fetch_dc.preflight_dc_access("sid", "my-org")
self.assertEqual(url, "https://example.salesforce.com")
self.assertEqual(token, "TOKEN")
def test_raises_dc_access_denied_on_401(self):
with mock.patch.object(
fetch_dc, "resolve_org",
return_value=("https://x", "T"),
):
with mock.patch.object(
fetch_dc, "post",
side_effect=DCQueryError("http=401 Unauthorized"),
):
with self.assertRaises(fetch_dc.DcAccessDenied) as ctx:
fetch_dc.preflight_dc_access("sid", "my-org")
self.assertEqual(ctx.exception.reason, "401")
def test_raises_dc_access_denied_on_403(self):
with mock.patch.object(
fetch_dc, "resolve_org",
return_value=("https://x", "T"),
):
with mock.patch.object(
fetch_dc, "post",
side_effect=DCQueryError("http=403 Forbidden"),
):
with self.assertRaises(fetch_dc.DcAccessDenied) as ctx:
fetch_dc.preflight_dc_access("sid", "my-org")
self.assertEqual(ctx.exception.reason, "403")
def test_raises_dc_access_denied_on_other_dc_error(self):
with mock.patch.object(
fetch_dc, "resolve_org",
return_value=("https://x", "T"),
):
with mock.patch.object(
fetch_dc, "post",
side_effect=DCQueryError("http=500 ISE"),
):
with self.assertRaises(fetch_dc.DcAccessDenied) as ctx:
fetch_dc.preflight_dc_access("sid", "my-org")
# 5xx falls into the catch-all dc_probe_failed bucket.
self.assertEqual(ctx.exception.reason, "dc_probe_failed")
# -----------------------------------------------------------------------------
# _emit_dc_access_denied_preamble — JSON shape
# -----------------------------------------------------------------------------
class EmitPreambleTests(unittest.TestCase):
def test_emits_single_line_json_with_two_options(self):
buf = io.StringIO()
with mock.patch("sys.stdout", buf):
fetch_dc._emit_dc_access_denied_preamble("401", "Unauthorized")
printed = buf.getvalue().strip()
# One single-line JSON record
self.assertEqual(printed.count("\n"), 0)
payload = json.loads(printed)
self.assertEqual(payload["status"], "DC_ACCESS_DENIED")
self.assertEqual(payload["reason"], "401")
self.assertEqual(payload["detail"], "Unauthorized")
# Standalone d360 has retry + cancel only — no sibling-skill switch.
self.assertEqual(len(payload["options"]), 2)
codes = [o["code"] for o in payload["options"]]
self.assertEqual(codes, ["1", "2"])
actions = [o["action"] for o in payload["options"]]
self.assertEqual(actions, ["retry", "cancel"])
# -----------------------------------------------------------------------------
# _handle_dc_access_denied — branches on tty vs not, + interactive choices
# -----------------------------------------------------------------------------
class HandleDcAccessDeniedTests(unittest.TestCase):
def _exc(self) -> fetch_dc.DcAccessDenied:
return fetch_dc.DcAccessDenied("401", "Unauthorized")
def test_headless_emits_preamble_and_returns_exit_code(self):
with mock.patch.object(
fetch_dc, "_emit_dc_access_denied_preamble"
) as emit:
rc = fetch_dc._handle_dc_access_denied(
self._exc(), session_id="sid", is_tty=False,
)
emit.assert_called_once_with("401", "Unauthorized")
self.assertEqual(rc, fetch_dc.EXIT_DC_ACCESS_DENIED)
def test_interactive_choice_1_returns_exit_code(self):
# User picks "1" → caller signaled to retry.
with mock.patch.object(fetch_dc.sys, "stdin", io.StringIO("1\n")):
with mock.patch.object(fetch_dc, "_log"):
rc = fetch_dc._handle_dc_access_denied(
self._exc(), session_id="sid", is_tty=True,
)
self.assertEqual(rc, fetch_dc.EXIT_DC_ACCESS_DENIED)
def test_interactive_choice_2_returns_zero_cancel(self):
# Standalone d360: choice "2" is cancel (no sibling-skill switch).
with mock.patch.object(fetch_dc.sys, "stdin", io.StringIO("2\n")):
with mock.patch.object(fetch_dc, "_log"):
rc = fetch_dc._handle_dc_access_denied(
self._exc(), session_id="sid", is_tty=True,
)
self.assertEqual(rc, 0)
def test_interactive_unknown_choice_treated_as_cancel(self):
# "" / "9" / random → cancel.
with mock.patch.object(fetch_dc.sys, "stdin", io.StringIO("\n")):
with mock.patch.object(fetch_dc, "_log"):
rc = fetch_dc._handle_dc_access_denied(
self._exc(), session_id="sid", is_tty=True,
)
self.assertEqual(rc, 0)
def test_interactive_keyboard_interrupt_returns_zero(self):
# stdin.readline() raising KeyboardInterrupt → graceful 0.
fake_stdin = mock.MagicMock()
fake_stdin.readline.side_effect = KeyboardInterrupt
with mock.patch.object(fetch_dc.sys, "stdin", fake_stdin):
with mock.patch.object(fetch_dc, "_log"):
rc = fetch_dc._handle_dc_access_denied(
self._exc(), session_id="sid", is_tty=True,
)
self.assertEqual(rc, 0)
# -----------------------------------------------------------------------------
# _in_list — SQL IN-clause fragment builder
# -----------------------------------------------------------------------------
class InListTests(unittest.TestCase):
def test_renders_quoted_csv_inside_parens(self):
self.assertEqual(fetch_dc._in_list(["a", "b", "c"]), "('a','b','c')")
def test_dedups_and_preserves_first_occurrence_order(self):
self.assertEqual(
fetch_dc._in_list(["a", "b", "a", "c", "b"]), "('a','b','c')"
)
def test_drops_empty_string(self):
self.assertEqual(fetch_dc._in_list(["a", "", "b"]), "('a','b')")
def test_drops_NOT_SET_token(self):
self.assertEqual(
fetch_dc._in_list(["a", "NOT_SET", "b"]), "('a','b')"
)
def test_empty_input_returns_empty_parens(self):
# All inputs filtered → "()". SQL won't accept this but that's the
# caller's concern — the helper itself is mechanical.
self.assertEqual(fetch_dc._in_list([]), "()")
self.assertEqual(fetch_dc._in_list(["", "NOT_SET"]), "()")
# -----------------------------------------------------------------------------
# _extract_trace_ids — DMO field + HTML-escaped fallback
# -----------------------------------------------------------------------------
class ExtractTraceIdsTests(unittest.TestCase):
def test_uses_telemetry_trace_id_when_populated(self):
rows = [{"ssot__TelemetryTraceId__c": "abc123"}]
self.assertEqual(fetch_dc._extract_trace_ids(rows), ["abc123"])
def test_falls_back_to_attribute_text_internalTraceId(self):
# Simulate the HTML-escaped JSON-in-a-string shape DC stores.
rows = [{
"ssot__TelemetryTraceId__c": "",
"ssot__AttributeText__c": '"internalTraceId":"deadbeef"',
}]
self.assertEqual(fetch_dc._extract_trace_ids(rows), ["deadbeef"])
def test_dedupes_preserving_first_occurrence(self):
rows = [
{"ssot__TelemetryTraceId__c": "x"},
{"ssot__TelemetryTraceId__c": "y"},
{"ssot__TelemetryTraceId__c": "x"},
]
self.assertEqual(fetch_dc._extract_trace_ids(rows), ["x", "y"])
def test_drops_NOT_SET_token(self):
rows = [{"ssot__TelemetryTraceId__c": "NOT_SET"}]
self.assertEqual(fetch_dc._extract_trace_ids(rows), [])
def test_returns_empty_when_no_trace_id_anywhere(self):
rows = [{"ssot__TelemetryTraceId__c": "", "ssot__AttributeText__c": ""}]
self.assertEqual(fetch_dc._extract_trace_ids(rows), [])
# -----------------------------------------------------------------------------
# _preflight_templates — SQL templates exist on disk
# -----------------------------------------------------------------------------
class PreflightTemplatesTests(unittest.TestCase):
def test_succeeds_when_all_templates_present(self):
# All the production .sql files ship in assets/dc/. This call should
# complete cleanly under the in-tree install.
fetch_dc._preflight_templates() # raises if any missing
if __name__ == "__main__":
unittest.main()