Files
cleveragents-core/features/steps/security_template_steps.py
2026-02-25 10:06:27 +00:00

244 lines
8.8 KiB
Python

"""Step definitions for secure template rendering tests."""
from __future__ import annotations
from behave import given, then, when
from behave.runner import Context
from cleveragents.templates.renderer import TemplateRenderer
from cleveragents.templates.secure_renderer import (
SecureTemplateRenderer,
TemplateConfig,
TemplateError,
TemplateSecurityError,
TemplateSizeError,
TemplateValidationError,
render_template_secure,
validate_template,
)
__all__: list[str] = []
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a secure template test environment")
def step_given_secure_template_env(context: Context) -> None:
context.sec_template = ""
context.sec_context = {} # dict[str, str]
context.sec_rendered = ""
context.sec_error = None # TemplateError | None
context.sec_validation_errors = [] # list[str]
context.sec_config = None # TemplateConfig | None
context.sec_allowed_keys = None # frozenset[str] | None
context.sec_renderer = None # SecureTemplateRenderer | None
# ---------------------------------------------------------------------------
# Given steps — template text
# ---------------------------------------------------------------------------
@given('the secure template text "{template}"')
def step_given_template(context: Context, template: str) -> None:
context.sec_template = template
@given('a secure template context key "{key}" with value "{value}"')
def step_given_context_key(context: Context, key: str, value: str) -> None:
context.sec_context[key] = value
@given('a secure template with allowed keys "{keys}"')
def step_given_allowed_keys(context: Context, keys: str) -> None:
context.sec_allowed_keys = frozenset(k.strip() for k in keys.split(","))
@given("the secure template has no key restrictions")
def step_given_template_open(context: Context) -> None:
context.sec_allowed_keys = None
@given("a secure template that is {n} characters long")
def step_given_long_template(context: Context, n: str) -> None:
context.sec_template = "x" * int(n)
@given("a secure template config with max output length of {n}")
def step_given_max_output_config(context: Context, n: str) -> None:
context.sec_config = TemplateConfig(max_output_length=int(n))
@given("a secure template config with logging disabled")
def step_given_config_logging_disabled(context: Context) -> None:
context.sec_config = TemplateConfig(
allowed_keys=context.sec_allowed_keys or frozenset({"name"}),
log_rejected=False,
)
@given("a secure template config that permits unknown keys")
def step_given_config_permit_unknown(context: Context) -> None:
context.sec_config = TemplateConfig(
allowed_keys=frozenset({"name"}),
reject_unknown_keys=False,
)
# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------
@when("I render the secure template")
def step_when_render_securely(context: Context) -> None:
config = context.sec_config
if config is None and context.sec_allowed_keys is not None:
config = TemplateConfig(allowed_keys=context.sec_allowed_keys)
renderer = SecureTemplateRenderer(config=config)
context.sec_rendered = renderer.render(context.sec_template, context.sec_context)
@when("I render the secure template expecting a security error")
def step_when_render_security_error(context: Context) -> None:
config = context.sec_config or TemplateConfig()
renderer = SecureTemplateRenderer(config=config)
try:
renderer.render(context.sec_template, context.sec_context)
context.sec_error = None
except TemplateError as exc:
assert isinstance(exc, TemplateSecurityError), (
f"Expected TemplateSecurityError, got {type(exc).__name__}: {exc}"
)
context.sec_error = exc
@when("I render the secure template expecting a size error")
def step_when_render_size_error(context: Context) -> None:
config = context.sec_config or TemplateConfig()
renderer = SecureTemplateRenderer(config=config)
try:
renderer.render(context.sec_template, context.sec_context)
context.sec_error = None
except TemplateError as exc:
assert isinstance(exc, TemplateSizeError), (
f"Expected TemplateSizeError, got {type(exc).__name__}: {exc}"
)
context.sec_error = exc
@when("I render the secure template expecting a validation error")
def step_when_render_validation_error(context: Context) -> None:
config = context.sec_config
if config is None and context.sec_allowed_keys is not None:
config = TemplateConfig(allowed_keys=context.sec_allowed_keys)
elif config is None:
config = TemplateConfig()
renderer = SecureTemplateRenderer(config=config)
try:
renderer.render(context.sec_template, context.sec_context)
context.sec_error = None
except TemplateError as exc:
assert isinstance(exc, TemplateValidationError), (
f"Expected TemplateValidationError, got {type(exc).__name__}: {exc}"
)
context.sec_error = exc
@when("I validate the secure template")
def step_when_validate_template(context: Context) -> None:
allowed = context.sec_allowed_keys or frozenset()
context.sec_validation_errors = validate_template(
context.sec_template, allowed_keys=allowed
)
@when("I render via the secure convenience function")
def step_when_render_convenience(context: Context) -> None:
context.sec_rendered = render_template_secure(
context.sec_template, context.sec_context
)
@when("I render via the secure convenience function expecting an error")
def step_when_render_convenience_error(context: Context) -> None:
try:
render_template_secure(context.sec_template, context.sec_context)
context.sec_error = None
except TemplateError as exc:
context.sec_error = exc
@when("I create a renderer with those settings")
def step_when_create_renderer(context: Context) -> None:
config = context.sec_config
if config is None and context.sec_allowed_keys is not None:
config = TemplateConfig(allowed_keys=context.sec_allowed_keys)
context.sec_renderer = SecureTemplateRenderer(config=config)
@when("I render via the legacy TemplateRenderer")
def step_when_render_legacy(context: Context) -> None:
renderer = TemplateRenderer()
context.sec_rendered = renderer.render(context.sec_template, context.sec_context)
@when("I render via the legacy TemplateRenderer with config")
def step_when_render_legacy_with_config(context: Context) -> None:
config = context.sec_config
if config is None and context.sec_allowed_keys is not None:
config = TemplateConfig(allowed_keys=context.sec_allowed_keys)
renderer = TemplateRenderer(config=config)
context.sec_rendered = renderer.render(context.sec_template, context.sec_context)
# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------
@then('the secure template output should be "{expected}"')
def step_then_rendered_output(context: Context, expected: str) -> None:
assert context.sec_rendered == expected, (
f"Expected {expected!r}, got {context.sec_rendered!r}"
)
@then('the secure template error should mention "{text}"')
def step_then_error_mentions(context: Context, text: str) -> None:
assert context.sec_error is not None, "Expected a template error"
assert text in str(context.sec_error), (
f"Expected '{text}' in error: {context.sec_error}"
)
@then("the secure template validation should be empty")
def step_then_validation_empty(context: Context) -> None:
assert context.sec_validation_errors == [], (
f"Expected no errors, got: {context.sec_validation_errors}"
)
@then("the secure template validation should not be empty")
def step_then_validation_not_empty(context: Context) -> None:
assert len(context.sec_validation_errors) > 0, (
"Expected validation errors but got none"
)
@then('the secure template validation should mention "{text}"')
def step_then_validation_mentions(context: Context, text: str) -> None:
combined = " ".join(context.sec_validation_errors)
assert text in combined, f"Expected '{text}' in: {context.sec_validation_errors}"
@then('the renderer config allowed keys should contain "{key}"')
def step_then_config_has_key(context: Context, key: str) -> None:
cfg = context.sec_renderer.config # exercises the .config property (line 155)
assert key in cfg.allowed_keys, (
f"Expected '{key}' in allowed_keys, got: {cfg.allowed_keys}"
)