e8bd348c77
CI / lint (pull_request) Successful in 41s
CI / typecheck (pull_request) Successful in 56s
CI / security (pull_request) Successful in 51s
CI / quality (pull_request) Successful in 53s
CI / integration_tests (pull_request) Successful in 1m10s
CI / build (pull_request) Successful in 43s
CI / unit_tests (pull_request) Successful in 3m30s
CI / coverage (pull_request) Successful in 3m22s
CI / status-check (pull_request) Successful in 4s
CI / quality (push) Successful in 45s
CI / security (push) Successful in 48s
CI / lint (push) Successful in 49s
CI / typecheck (push) Successful in 50s
CI / build (push) Successful in 49s
CI / integration_tests (push) Successful in 1m9s
CI / unit_tests (push) Successful in 3m13s
CI / coverage (push) Successful in 3m7s
CI / status-check (push) Successful in 3s
Add RegistryError base with message, details, and original_reference fields. Implement all 9 typed exception types per Package Registry Standard §13.2: PackageNotFoundError, InvalidPackageIdError, InvalidPackageReferenceError, VersionNotFoundError, ValidationError, AuthenticationRequiredError, AccessDeniedError, ConflictError, and RegistryNetworkError. RegistryNetworkError carries status_code and url for transport failures. __str__ includes original_reference when available. exception_for_status() maps HTTP codes to typed exceptions. _ERROR_TYPE_MAP enables error-type parsing from JSON error bodies. 23 Behave BDD scenarios in features/registry_exceptions.feature. 12 Robot Framework integration tests in robot/exceptions.robot. Existing registry_http_client tests updated for InternalServerError removal. ISSUES CLOSED: #29
301 lines
10 KiB
Python
301 lines
10 KiB
Python
"""
|
|
Step definitions for Registry Exception Hierarchy BDD tests.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import cast
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveractors.core.exceptions import CleverAgentsException
|
|
from cleveractors.registry.exceptions import (
|
|
_ERROR_TYPE_MAP,
|
|
AccessDeniedError,
|
|
AuthenticationRequiredError,
|
|
ConflictError,
|
|
InvalidPackageIdError,
|
|
InvalidPackageReferenceError,
|
|
PackageNotFoundError,
|
|
RegistryError,
|
|
RegistryNetworkError,
|
|
ValidationError,
|
|
VersionNotFoundError,
|
|
exception_for_status,
|
|
)
|
|
|
|
_CLASS_MAP: dict[str, type[CleverAgentsException]] = {
|
|
"CleverAgentsException": CleverAgentsException,
|
|
"RegistryError": RegistryError,
|
|
"PackageNotFoundError": PackageNotFoundError,
|
|
"InvalidPackageIdError": InvalidPackageIdError,
|
|
"InvalidPackageReferenceError": InvalidPackageReferenceError,
|
|
"VersionNotFoundError": VersionNotFoundError,
|
|
"ValidationError": ValidationError,
|
|
"AuthenticationRequiredError": AuthenticationRequiredError,
|
|
"AccessDeniedError": AccessDeniedError,
|
|
"ConflictError": ConflictError,
|
|
"RegistryNetworkError": RegistryNetworkError,
|
|
}
|
|
|
|
|
|
@given("I have imported the registry exception module")
|
|
def step_import_exception_module(context: Context) -> None:
|
|
context._checks_passed = 0
|
|
|
|
|
|
# ── Construction steps ──────────────────────────────────────────────────────
|
|
|
|
|
|
@when('I raise a plain RegistryError with text "{message}"')
|
|
def step_raise_registry_error_message(context: Context, message: str) -> None:
|
|
context.error = RegistryError(message)
|
|
|
|
|
|
@when('I raise a RegistryError text "{message}" and detail {details_json}')
|
|
def step_raise_registry_error_message_details(
|
|
context: Context, message: str, details_json: str
|
|
) -> None:
|
|
context.error = RegistryError(message, details=json.loads(details_json))
|
|
|
|
|
|
@when('I raise a RegistryError text "{message}" and reference "{ref}"')
|
|
def step_raise_registry_error_message_ref(
|
|
context: Context, message: str, ref: str
|
|
) -> None:
|
|
context.error = RegistryError(message, original_reference=ref)
|
|
|
|
|
|
@when(
|
|
'I raise a RegistryError text "{message}" detail {details_json} and reference "{ref}"'
|
|
)
|
|
def step_raise_registry_error_full(
|
|
context: Context, message: str, details_json: str, ref: str
|
|
) -> None:
|
|
context.error = RegistryError(
|
|
message, details=json.loads(details_json), original_reference=ref
|
|
)
|
|
|
|
|
|
@when(
|
|
'I raise a simple network error message "{message}" status {status_code} url "{url}"'
|
|
)
|
|
def step_raise_network_error_basic(
|
|
context: Context, message: str, status_code: str, url: str
|
|
) -> None:
|
|
sc = int(status_code)
|
|
context.error = RegistryNetworkError(message, status_code=sc, url=url)
|
|
|
|
|
|
@when(
|
|
'I raise a fully detailed network error message "{message}" detail {details_json} ref "{ref}" status {status_code} url "{url}"'
|
|
)
|
|
def step_raise_network_error_full(
|
|
context: Context,
|
|
message: str,
|
|
details_json: str,
|
|
ref: str,
|
|
status_code: str,
|
|
url: str,
|
|
) -> None:
|
|
sc = None if status_code == "None" else int(status_code)
|
|
context.error = RegistryNetworkError(
|
|
message,
|
|
details=json.loads(details_json),
|
|
original_reference=ref,
|
|
status_code=sc,
|
|
url=url,
|
|
)
|
|
|
|
|
|
# ── Assertion steps ─────────────────────────────────────────────────────────
|
|
|
|
|
|
@then('the error message should be "{expected}"')
|
|
def step_error_message(context: Context, expected: str) -> None:
|
|
assert context.error.message == expected, (
|
|
f"Expected message {expected!r}, got {context.error.message!r}"
|
|
)
|
|
|
|
|
|
@then("the error should have no details")
|
|
def step_error_no_details(context: Context) -> None:
|
|
assert context.error.details is None, (
|
|
f"Expected details=None, got {context.error.details!r}"
|
|
)
|
|
|
|
|
|
@then("the error should have no original_reference")
|
|
def step_error_no_ref(context: Context) -> None:
|
|
assert context.error.original_reference is None, (
|
|
f"Expected original_reference=None, got {context.error.original_reference!r}"
|
|
)
|
|
|
|
|
|
@then("the error details should equal {expected_json}")
|
|
def step_error_details(context: Context, expected_json: str) -> None:
|
|
expected = json.loads(expected_json)
|
|
assert context.error.details == expected, (
|
|
f"Expected details {expected!r}, got {context.error.details!r}"
|
|
)
|
|
|
|
|
|
@then('the error original_reference should be "{expected}"')
|
|
def step_error_ref(context: Context, expected: str) -> None:
|
|
assert context.error.original_reference == expected, (
|
|
f"Expected original_reference {expected!r}, "
|
|
f"got {context.error.original_reference!r}"
|
|
)
|
|
|
|
|
|
@then('str of the error should be "{expected}"')
|
|
def step_error_str(context: Context, expected: str) -> None:
|
|
assert str(context.error) == expected, (
|
|
f"Expected str {expected!r}, got {str(context.error)!r}"
|
|
)
|
|
|
|
|
|
@then("the error status_code should be {expected}")
|
|
def step_error_status_code(context: Context, expected: str) -> None:
|
|
exp = None if expected == "None" else int(expected)
|
|
assert context.error.status_code == exp, (
|
|
f"Expected status_code {exp!r}, got {context.error.status_code!r}"
|
|
)
|
|
|
|
|
|
@then('the error url should be "{expected}"')
|
|
def step_error_url(context: Context, expected: str) -> None:
|
|
assert context.error.url == expected, (
|
|
f"Expected url {expected!r}, got {context.error.url!r}"
|
|
)
|
|
|
|
|
|
# ── exception_for_status steps ──────────────────────────────────────────────
|
|
|
|
|
|
@when('I call exception_for_status with code {code:d} and message "{message}"')
|
|
def step_call_exception_for_status(context: Context, code: int, message: str) -> None:
|
|
context.result = exception_for_status(code, message)
|
|
|
|
|
|
@when(
|
|
'I call exception_for_status with non-integer code "{code}" and message "{message}"'
|
|
)
|
|
def step_call_exception_for_status_non_int(
|
|
context: Context, code: str, message: str
|
|
) -> None:
|
|
# Intentionally non-int to exercise the isinstance guard in exception_for_status
|
|
context.result = exception_for_status(cast(int, code), message)
|
|
|
|
|
|
@then("the result should be an instance of {cls_name}")
|
|
def step_result_instance_of(context: Context, cls_name: str) -> None:
|
|
expected_cls = _CLASS_MAP[cls_name]
|
|
assert isinstance(context.result, expected_cls), (
|
|
f"Expected isinstance of {expected_cls.__name__}, "
|
|
f"got {type(context.result).__name__}"
|
|
)
|
|
context.error = context.result
|
|
|
|
|
|
# ── Inheritance chain steps ─────────────────────────────────────────────────
|
|
|
|
|
|
@when("I check that {child} is a subclass of {parent}")
|
|
def step_check_subclass(context: Context, child: str, parent: str) -> None:
|
|
child_cls = _CLASS_MAP[child]
|
|
parent_cls = _CLASS_MAP[parent]
|
|
assert issubclass(child_cls, parent_cls), (
|
|
f"{child_cls.__name__} is not a subclass of {parent_cls.__name__}"
|
|
)
|
|
context._checks_passed += 1
|
|
|
|
|
|
@then("all {count:d} checks should pass")
|
|
def step_all_checks_pass(context: Context, count: int) -> None:
|
|
assert context._checks_passed == count, (
|
|
f"Expected {count} inheritance checks, got {context._checks_passed}"
|
|
)
|
|
|
|
|
|
# ── Error type map steps ────────────────────────────────────────────────────
|
|
|
|
|
|
@when("I inspect the _ERROR_TYPE_MAP")
|
|
def step_inspect_map(context: Context) -> None:
|
|
context._error_map = _ERROR_TYPE_MAP
|
|
|
|
|
|
@then('the map should contain key "{key}"')
|
|
def step_map_has_key(context: Context, key: str) -> None:
|
|
assert key in context._error_map, f"Expected _ERROR_TYPE_MAP to contain key {key!r}"
|
|
|
|
|
|
@then("the map should have exactly {count:d} keys")
|
|
def step_map_cardinality(context: Context, count: int) -> None:
|
|
actual = len(context._error_map)
|
|
assert actual == count, (
|
|
f"Expected _ERROR_TYPE_MAP to have {count} keys, got {actual}"
|
|
)
|
|
|
|
|
|
@then('the value for key "{key}" should be {value_cls}')
|
|
def step_map_value(context: Context, key: str, value_cls: str) -> None:
|
|
expected = _CLASS_MAP[value_cls]
|
|
assert context._error_map[key] is expected, (
|
|
f"Expected _ERROR_TYPE_MAP[{key!r}] = {expected.__name__}, "
|
|
f"got {context._error_map[key].__name__}"
|
|
)
|
|
|
|
|
|
# ── Typed subclass construction steps ────────────────────────────────────────
|
|
|
|
|
|
@when('I raise a PackageNotFoundError with message "{message}"')
|
|
def step_raise_package_not_found_error(context: Context, message: str) -> None:
|
|
context.error = PackageNotFoundError(message)
|
|
|
|
|
|
@when('I raise an InvalidPackageIdError with message "{message}"')
|
|
def step_raise_invalid_id_error(context: Context, message: str) -> None:
|
|
context.error = InvalidPackageIdError(message)
|
|
|
|
|
|
@when('I raise an InvalidPackageReferenceError with message "{message}"')
|
|
def step_raise_invalid_ref_error(context: Context, message: str) -> None:
|
|
context.error = InvalidPackageReferenceError(message)
|
|
|
|
|
|
@when('I raise a VersionNotFoundError with message "{message}"')
|
|
def step_raise_version_not_found_error(context: Context, message: str) -> None:
|
|
context.error = VersionNotFoundError(message)
|
|
|
|
|
|
@when('I raise a ValidationError with message "{message}"')
|
|
def step_raise_validation_error(context: Context, message: str) -> None:
|
|
context.error = ValidationError(message)
|
|
|
|
|
|
@when('I raise an AuthenticationRequiredError with message "{message}"')
|
|
def step_raise_auth_required_error(context: Context, message: str) -> None:
|
|
context.error = AuthenticationRequiredError(message)
|
|
|
|
|
|
@when('I raise an AccessDeniedError with message "{message}"')
|
|
def step_raise_access_denied_error(context: Context, message: str) -> None:
|
|
context.error = AccessDeniedError(message)
|
|
|
|
|
|
@when('I raise a ConflictError with message "{message}"')
|
|
def step_raise_conflict_error(context: Context, message: str) -> None:
|
|
context.error = ConflictError(message)
|
|
|
|
|
|
@then("the error should be an instance of {cls_name}")
|
|
def step_error_is_instance(context: Context, cls_name: str) -> None:
|
|
assert isinstance(context.error, _CLASS_MAP[cls_name]), (
|
|
f"Expected {cls_name}, got {type(context.error).__name__}"
|
|
)
|