97c1007bb5
CI / push-validation (pull_request) Successful in 48s
CI / build (pull_request) Successful in 1m17s
CI / helm (pull_request) Successful in 1m0s
CI / lint (pull_request) Successful in 1m54s
CI / quality (pull_request) Successful in 1m55s
CI / security (pull_request) Successful in 2m5s
CI / typecheck (pull_request) Successful in 2m4s
CI / integration_tests (pull_request) Successful in 5m1s
CI / unit_tests (pull_request) Successful in 5m10s
CI / docker (pull_request) Successful in 1m51s
CI / coverage (pull_request) Successful in 12m47s
CI / status-check (pull_request) Successful in 8s
CI / benchmark-publish (push) Has started running
CI / benchmark-regression (push) Failing after 1m15s
CI / typecheck (push) Has started running
CI / security (push) Has started running
CI / quality (push) Has started running
CI / integration_tests (push) Has started running
CI / unit_tests (push) Has started running
CI / e2e_tests (push) Has started running
CI / lint (push) Successful in 56s
CI / helm (push) Successful in 53s
CI / push-validation (push) Successful in 54s
CI / build (push) Successful in 1m24s
CI / coverage (push) Blocked by required conditions
CI / docker (push) Blocked by required conditions
CI / status-check (push) Blocked by required conditions
Add TokenAuthMiddleware to emit AUTH_SUCCESS/AUTH_FAILURE with spec-aligned audit details and wire it through the DI container using server.token resolution. Add Behave and Robot coverage for auth event emission and end-to-end audit persistence, and update audit subscriber producer notes and changelog. ISSUES CLOSED: #714
166 lines
6.0 KiB
Python
166 lines
6.0 KiB
Python
"""Step definitions for auth_middleware_events.feature."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from cleveragents.application.services.audit_event_subscriber import (
|
|
AuditEventSubscriber,
|
|
)
|
|
from cleveragents.application.services.audit_service import AuditService
|
|
from cleveragents.application.services.auth_middleware import TokenAuthMiddleware
|
|
from cleveragents.config.settings import Settings
|
|
from cleveragents.infrastructure.database.models import Base
|
|
from cleveragents.infrastructure.events.reactive import ReactiveEventBus
|
|
from cleveragents.infrastructure.events.types import EventType
|
|
from features.mocks.recording_event_bus import RecordingEventBus
|
|
|
|
|
|
def _fresh_audit_service() -> AuditService:
|
|
Settings._instance = None
|
|
settings = Settings(database_url="sqlite:///:memory:")
|
|
engine = create_engine("sqlite:///:memory:", echo=False)
|
|
Base.metadata.create_all(engine)
|
|
session = sessionmaker(bind=engine)()
|
|
return AuditService(settings=settings, session=session)
|
|
|
|
|
|
@given('an auth middleware with expected token "{token}"')
|
|
def step_auth_middleware_with_token(context: Context, token: str) -> None:
|
|
context.expected_token = token
|
|
|
|
|
|
@given("an auth middleware with no configured token")
|
|
def step_auth_middleware_no_token(context: Context) -> None:
|
|
context.expected_token = None
|
|
|
|
|
|
@given("an auth recording event bus")
|
|
def step_auth_recording_event_bus(context: Context) -> None:
|
|
context.event_bus = RecordingEventBus()
|
|
context.auth_middleware = TokenAuthMiddleware(
|
|
expected_token=context.expected_token,
|
|
event_bus=context.event_bus,
|
|
)
|
|
context.auth_result_history = []
|
|
|
|
|
|
@given('auth middleware is wired to AuditEventSubscriber with expected token "{token}"')
|
|
def step_auth_middleware_audit_pipeline(context: Context, token: str) -> None:
|
|
context.audit_service = _fresh_audit_service()
|
|
context.event_bus = ReactiveEventBus()
|
|
context.audit_subscriber = AuditEventSubscriber(
|
|
audit_service=context.audit_service,
|
|
event_bus=context.event_bus,
|
|
)
|
|
context.auth_middleware = TokenAuthMiddleware(
|
|
expected_token=token,
|
|
event_bus=context.event_bus,
|
|
)
|
|
context.auth_result_history = []
|
|
|
|
|
|
@when('I authenticate with token "{token}" identity "{identity}" and ip "{ip_address}"')
|
|
def step_authenticate(
|
|
context: Context, token: str, identity: str, ip_address: str
|
|
) -> None:
|
|
context.auth_result = context.auth_middleware.authenticate(
|
|
token,
|
|
identity=identity,
|
|
ip_address=ip_address,
|
|
)
|
|
context.auth_result_history.append(context.auth_result)
|
|
|
|
|
|
@when("I try to authenticate with an empty token")
|
|
def step_try_authenticate_empty_token(context: Context) -> None:
|
|
context.auth_error = None
|
|
try:
|
|
context.auth_middleware.authenticate("", identity="empty@example.com")
|
|
except ValueError as exc:
|
|
context.auth_error = exc
|
|
|
|
|
|
@then("the auth result should be true")
|
|
def step_auth_result_true(context: Context) -> None:
|
|
assert context.auth_result is True
|
|
|
|
|
|
@then("the auth result should be false")
|
|
def step_auth_result_false(context: Context) -> None:
|
|
assert context.auth_result is False
|
|
|
|
|
|
@then("the auth event bus should have exactly {count:d} event")
|
|
def step_auth_event_count(context: Context, count: int) -> None:
|
|
assert len(context.event_bus.events) == count, (
|
|
f"Expected {count} events, got {len(context.event_bus.events)}"
|
|
)
|
|
|
|
|
|
@then("the latest auth event type should be {event_type_name}")
|
|
def step_latest_auth_event_type(context: Context, event_type_name: str) -> None:
|
|
expected = EventType[event_type_name]
|
|
actual = context.event_bus.events[-1].event_type
|
|
assert actual == expected, f"Expected event type {expected}, got {actual}"
|
|
|
|
|
|
@then(
|
|
'the latest auth event should contain detail "{key}" with value "{expected_value}"'
|
|
)
|
|
def step_latest_auth_event_detail(
|
|
context: Context, key: str, expected_value: str
|
|
) -> None:
|
|
details = context.event_bus.events[-1].details
|
|
assert details.get(key) == expected_value, (
|
|
f"Expected {key}={expected_value!r}, got {details.get(key)!r}"
|
|
)
|
|
|
|
|
|
@then("an auth ValueError should be raised")
|
|
def step_auth_value_error(context: Context) -> None:
|
|
assert context.auth_error is not None, (
|
|
"Expected ValueError but no exception was raised"
|
|
)
|
|
assert isinstance(context.auth_error, ValueError), (
|
|
f"Expected ValueError, got {type(context.auth_error).__name__}"
|
|
)
|
|
|
|
|
|
@then('the auth result history should be "{history_csv}"')
|
|
def step_auth_result_history(context: Context, history_csv: str) -> None:
|
|
expected = [part.strip().lower() == "true" for part in history_csv.split(",")]
|
|
assert context.auth_result_history == expected, (
|
|
f"Expected history {expected}, got {context.auth_result_history}"
|
|
)
|
|
|
|
|
|
@then("the audit log should contain 1 auth_success entry from middleware")
|
|
def step_audit_has_auth_success(context: Context) -> None:
|
|
entries = context.audit_service.list_entries(event_type="auth_success")
|
|
assert len(entries) == 1, f"Expected 1 auth_success entry, got {len(entries)}"
|
|
|
|
|
|
@then("the audit log should contain 1 auth_failure entry from middleware")
|
|
def step_audit_has_auth_failure(context: Context) -> None:
|
|
entries = context.audit_service.list_entries(event_type="auth_failure")
|
|
assert len(entries) == 1, f"Expected 1 auth_failure entry, got {len(entries)}"
|
|
|
|
|
|
@then(
|
|
'the latest {audit_event_type} audit entry should contain detail "{key}" with value "{expected_value}"'
|
|
)
|
|
def step_latest_audit_entry_detail(
|
|
context: Context, audit_event_type: str, key: str, expected_value: str
|
|
) -> None:
|
|
entries = context.audit_service.list_entries(event_type=audit_event_type)
|
|
assert entries, f"Expected at least one {audit_event_type} audit entry"
|
|
details = entries[0].details
|
|
assert details.get(key) == expected_value, (
|
|
f"Expected {audit_event_type}.{key}={expected_value!r}, "
|
|
f"got {details.get(key)!r}"
|
|
)
|