Files
OSIT-AE-API-FastAPI/tests/verify_feedback_fixes.py
Scott Idem 34a752d455 feat(api-v3): implement permissive updates, automatic ID resolution, and structured error reporting
- Added 'x-ae-ignore-extra-fields' header to support stripping unknown fields in POST/PATCH.
- Added automatic resolution of '*_id_random' strings to integer IDs in 'sanitize_payload'.
- Refactored 'post_obj' to return structured (field -> message) validation errors in 'meta.details'.
- Updated 'mk_resp' to support non-string 'details' in response metadata.
- Added 'tests/verify_feedback_fixes.py' to validate logic changes.

Ref: V3 API Refinement Feedback from mcp_agent.
2026-01-14 19:11:56 -05:00

104 lines
3.4 KiB
Python

import sys
import os
from unittest.mock import MagicMock
# --- Environment Setup ---
sys.modules['redis'] = MagicMock()
sys.modules['sqlalchemy'] = MagicMock()
sys.modules['app.config'] = MagicMock()
sys.modules['html2text'] = MagicMock()
sys.modules['app.log'] = MagicMock()
sys.modules['app.lib_general'] = MagicMock()
# Mock app.db_sql
mock_db_sql = MagicMock()
# Mock ID resolution: abc -> 123
mock_db_sql.redis_lookup_id_random.side_effect = lambda record_id_random, table_name: 123 if record_id_random == 'abc' else None
sys.modules['app.db_sql'] = mock_db_sql
# Add project root to path
sys.path.append(os.getcwd())
from app.lib_api_crud_v3 import sanitize_payload
from pydantic import BaseModel, Field, ValidationError
from typing import Optional, List, ClassVar
class MockModel(BaseModel):
id: Optional[int]
name: str = Field(None, min_length=3)
account_id: Optional[int]
fields_to_exclude_from_db: ClassVar[List[str]] = ['computed_field']
def test_permissive_update():
print("--- Testing Permissive Update (ignore_extra=True) ---")
payload = {
"name": "Test",
"extra_field": "Should be removed",
"computed_field": "Should be removed"
}
sanitize_payload(payload, MockModel, ignore_extra=True)
print(f"Sanitized Payload: {payload}")
assert "extra_field" not in payload
assert "computed_field" not in payload
assert payload["name"] == "Test"
print("✅ Permissive update stripping works.")
def test_strict_update():
print("\n--- Testing Strict Update (ignore_extra=False) ---")
payload = {
"name": "Test",
"extra_field": "Should be removed",
"computed_field": "Should be removed"
}
sanitize_payload(payload, MockModel, ignore_extra=False)
print(f"Sanitized Payload: {payload}")
assert "extra_field" in payload
assert "computed_field" not in payload
print("✅ Strict update correctly preserves unknown fields (waiting for DB error) but strips excluded fields.")
def test_id_resolution():
print("\n--- Testing ID Resolution ---")
payload = {
"name": "Test",
"account_id_random": "abc"
}
sanitize_payload(payload, MockModel)
print(f"Sanitized Payload: {payload}")
assert payload.get("account_id") == 123
assert "account_id_random" not in payload
print("✅ ID resolution (account_id_random -> account_id) works.")
def test_structured_validation_errors():
print("\n--- Testing Structured Validation Errors ---")
payload = {
"name": "a" # Too short
}
try:
MockModel(**payload)
except ValidationError as e:
structured_errors = {err['loc'][-1]: err['msg'] for err in e.errors()}
print(f"Structured Errors: {structured_errors}")
assert "name" in structured_errors
# Pydantic 1.x error message
assert "at least 3 characters" in structured_errors["name"]
print("✅ Structured validation errors work.")
if __name__ == "__main__":
try:
test_permissive_update()
test_strict_update()
test_id_resolution()
test_structured_validation_errors()
print("\n🎉 All local logic tests passed!")
except AssertionError as e:
print(f"\n❌ Test failed: {e}")
sys.exit(1)
except Exception as e:
print(f"\n💥 An error occurred: {e}")
import traceback
traceback.print_exc()
sys.exit(1)