Skip to content

Commit 80b826e

Browse files
test: raise coverage 79% → 93% for OpenSSF Gold targets (#221)
* test: raise coverage 79% to 93% for OpenSSF Gold targets Dedicated coverage push (G3, docs/proposals/openssf-best-practices.md): 423 new unit tests across the least-covered modules, all deterministic, no network, mocked at the same boundaries existing tests use. Per-module statement coverage: - cli/main.py 50% -> 99% (94 tests: commands, dcv, keys, policy rollback, enforce pipeline) - mcp/server.py 30% -> 98% (100 tests: tool handlers, transports, policy enforcement, DCV tools) - core/invoke.py 55% -> 99% (A2A/MCP endpoint resolution, SDK path) - backends/infoblox/ 53%/70% -> 100%/100% (bloxone incl. Threat Defense, nios incl. RPZ suite) - backends/cloud_dns 53% -> 100%, utils/google_auth 21% -> 100% - cli/init 40% -> 98%, doctor 72% -> 99%, backends/base 73% -> 100%, backends/__init__ 74% -> 100%, sdk/policy/cel_evaluator 68% -> 98% Full suite: 2565 passed, 2 skipped (pre-existing live-endpoint skips). Total: 93% combined statement+branch, 94% statement-only - clears the Gold 90%/80% bars. Follow-up: ratchet ci.yml --cov-fail-under (added at 78 in #218) to 92 once both PRs land. Signed-off-by: Ingmar Van Glabbeek <ivanglabbeek@infoblox.com> * test: resolve CodeQL style alerts in new tests Single import style per module (importlib.import_module instead of dual import/import-from), string-form monkeypatch.setattr targets, and a direct types.ModuleType reference instead of a wrapper lambda. Signed-off-by: Ingmar Van Glabbeek <ivanglabbeek@infoblox.com> --------- Signed-off-by: Ingmar Van Glabbeek <ivanglabbeek@infoblox.com> Co-authored-by: Igor Racic <iracic82@gmail.com>
1 parent 03a2c5e commit 80b826e

12 files changed

Lines changed: 6205 additions & 2 deletions

tests/unit/cli/test_cli_main_extended.py

Lines changed: 1559 additions & 0 deletions
Large diffs are not rendered by default.

tests/unit/core/test_invoke.py

Lines changed: 811 additions & 0 deletions
Large diffs are not rendered by default.

tests/unit/mcp/test_server_tools.py

Lines changed: 1146 additions & 0 deletions
Large diffs are not rendered by default.

tests/unit/sdk/policy/test_cel_evaluator.py

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
from __future__ import annotations
77

8-
from unittest.mock import patch
8+
import importlib
9+
from unittest.mock import Mock, patch
910

1011
import pytest
1112
from pydantic import ValidationError
@@ -675,3 +676,135 @@ def test_circuit_combined_with_trust(self) -> None:
675676
"layer1",
676677
)
677678
assert len(v) == 1
679+
680+
681+
# =============================================================================
682+
# Backend selection and the pure-Python fallback backend
683+
# =============================================================================
684+
685+
686+
class TestBackendSelection:
687+
"""_select_backend priority: Rust first, cel-python fallback, else ImportError."""
688+
689+
def test_rust_backend_preferred_when_available(self) -> None:
690+
from dns_aid.sdk.policy.cel_evaluator import _RustBackend, _select_backend
691+
692+
backend = _select_backend()
693+
# In the dev environment both backends are installed → Rust wins.
694+
assert isinstance(backend, _RustBackend)
695+
696+
def test_python_fallback_when_rust_unavailable(self) -> None:
697+
mod = importlib.import_module("dns_aid.sdk.policy.cel_evaluator")
698+
699+
with patch.object(mod, "_RustBackend", side_effect=ImportError("no cel")):
700+
backend = mod._select_backend()
701+
assert isinstance(backend, mod._PythonBackend)
702+
703+
def test_no_backend_available_raises_import_error(self) -> None:
704+
mod = importlib.import_module("dns_aid.sdk.policy.cel_evaluator")
705+
706+
with (
707+
patch.object(mod, "_RustBackend", side_effect=ImportError("no cel")),
708+
patch.object(mod, "_PythonBackend", side_effect=ImportError("no celpy")),
709+
):
710+
with pytest.raises(ImportError, match="No CEL backend available"):
711+
mod._select_backend()
712+
713+
def test_evaluator_records_backend_name(self) -> None:
714+
from dns_aid.sdk.policy.cel_evaluator import CELRuleEvaluator
715+
716+
evaluator = CELRuleEvaluator()
717+
assert evaluator.backend_name in ("_RustBackend", "_PythonBackend")
718+
719+
720+
class TestPythonBackend:
721+
"""Exercise the cel-python (celpy) fallback backend directly."""
722+
723+
def _python_evaluator(self):
724+
mod = importlib.import_module("dns_aid.sdk.policy.cel_evaluator")
725+
726+
with patch.object(mod, "_RustBackend", side_effect=ImportError("no cel")):
727+
return mod.CELRuleEvaluator()
728+
729+
def test_compile_and_execute(self) -> None:
730+
from dns_aid.sdk.policy.cel_evaluator import _PythonBackend
731+
732+
backend = _PythonBackend()
733+
prog = backend.compile("request.caller_trust_score >= 50.0")
734+
assert bool(backend.execute(prog, {"caller_trust_score": 80.0})) is True
735+
assert bool(backend.execute(prog, {"caller_trust_score": 10.0})) is False
736+
737+
def test_dict_to_cel_map_type_coercion(self) -> None:
738+
from celpy import celtypes
739+
740+
from dns_aid.sdk.policy.cel_evaluator import _PythonBackend
741+
742+
cel_map = _PythonBackend._dict_to_cel_map(
743+
{"b": True, "i": 3, "f": 1.5, "s": "x", "n": None}, celtypes
744+
)
745+
assert cel_map[celtypes.StringType("b")] == celtypes.BoolType(True)
746+
assert isinstance(cel_map[celtypes.StringType("b")], celtypes.BoolType)
747+
assert isinstance(cel_map[celtypes.StringType("i")], celtypes.IntType)
748+
assert isinstance(cel_map[celtypes.StringType("f")], celtypes.DoubleType)
749+
assert cel_map[celtypes.StringType("s")] == celtypes.StringType("x")
750+
# Non-primitive values are stringified
751+
assert cel_map[celtypes.StringType("n")] == celtypes.StringType("None")
752+
753+
def test_evaluator_with_python_backend_denies(self) -> None:
754+
evaluator = self._python_evaluator()
755+
assert evaluator.backend_name == "_PythonBackend"
756+
rules = [
757+
CELRule(
758+
id="trust",
759+
expression="request.caller_trust_score >= 50.0",
760+
effect="deny",
761+
message="Too low",
762+
)
763+
]
764+
v, _ = evaluator.evaluate(rules, _ctx(caller_trust_score=10.0), "layer1")
765+
assert len(v) == 1
766+
assert v[0].rule == "cel:trust"
767+
768+
def test_evaluator_with_python_backend_allows(self) -> None:
769+
evaluator = self._python_evaluator()
770+
rules = [
771+
CELRule(
772+
id="proto",
773+
expression='request.protocol == "mcp" && request.dnssec_validated',
774+
effect="deny",
775+
)
776+
]
777+
v, _ = evaluator.evaluate(rules, _ctx(protocol="mcp", dnssec_validated=True), "layer1")
778+
assert len(v) == 0
779+
780+
def test_python_backend_bad_expression_fails_open(self) -> None:
781+
evaluator = self._python_evaluator()
782+
rules = [CELRule(id="bad", expression="!!! invalid CEL ???", effect="deny")]
783+
v, w = evaluator.evaluate(rules, _ctx(), "layer1")
784+
assert len(v) == 0
785+
assert len(w) == 0
786+
787+
788+
# =============================================================================
789+
# Negative compile cache
790+
# =============================================================================
791+
792+
793+
class TestNegativeCompileCache:
794+
"""Expressions that fail to compile are negatively cached."""
795+
796+
def test_recompile_of_bad_expression_raises_value_error(self) -> None:
797+
from dns_aid.sdk.policy.cel_evaluator import CELRuleEvaluator
798+
799+
evaluator = CELRuleEvaluator()
800+
evaluator._backend = Mock()
801+
evaluator._backend.compile = Mock(side_effect=RuntimeError("parse error"))
802+
803+
with pytest.raises(RuntimeError, match="parse error"):
804+
evaluator._compile("bad expression")
805+
assert "bad expression" in evaluator._bad_expressions
806+
807+
# Second attempt short-circuits without touching the backend again
808+
with pytest.raises(ValueError, match="Previously failed to compile"):
809+
evaluator._compile("bad expression")
810+
assert evaluator._backend.compile.call_count == 1

0 commit comments

Comments
 (0)