|
1 | 1 | """
|
2 | 2 | Unit tests for Bedrock Guardrails
|
3 | 3 | """
|
4 |
| - |
| 4 | +import json |
5 | 5 | import os
|
6 | 6 | import sys
|
7 | 7 | from unittest.mock import AsyncMock, MagicMock, patch
|
8 | 8 |
|
9 | 9 | import pytest
|
| 10 | +from fastapi import HTTPException |
10 | 11 |
|
11 | 12 | sys.path.insert(0, os.path.abspath("../../../../../.."))
|
12 | 13 |
|
@@ -238,7 +239,6 @@ async def test_bedrock_guardrail_logging_uses_redacted_response():
|
238 | 239 | ) as mock_load_creds, patch.object(
|
239 | 240 | guardrail, "_prepare_request", return_value=MagicMock()
|
240 | 241 | ) as mock_prepare_request:
|
241 |
| - |
242 | 242 | mock_post.return_value = mock_bedrock_response
|
243 | 243 |
|
244 | 244 | # Call the method that should log the redacted response
|
@@ -345,7 +345,6 @@ async def test_bedrock_guardrail_original_response_not_modified():
|
345 | 345 | ) as mock_load_creds, patch.object(
|
346 | 346 | guardrail, "_prepare_request", return_value=MagicMock()
|
347 | 347 | ) as mock_prepare_request:
|
348 |
| - |
349 | 348 | mock_post.return_value = mock_bedrock_response
|
350 | 349 |
|
351 | 350 | # Call the method
|
@@ -860,7 +859,6 @@ async def test__redact_pii_matches_comprehensive_coverage():
|
860 | 859 |
|
861 | 860 | print("Comprehensive coverage redaction test passed")
|
862 | 861 |
|
863 |
| - |
864 | 862 | @pytest.mark.asyncio
|
865 | 863 | async def test_bedrock_guardrail_respects_custom_runtime_endpoint(monkeypatch):
|
866 | 864 | """Test that BedrockGuardrail respects aws_bedrock_runtime_endpoint when set"""
|
@@ -1049,3 +1047,77 @@ async def test_bedrock_guardrail_parameter_takes_precedence_over_env(monkeypatch
|
1049 | 1047 | ), f"Expected parameter endpoint to take precedence. Got: {prepped_request.url}"
|
1050 | 1048 |
|
1051 | 1049 | print(f"Parameter precedence test passed. URL: {prepped_request.url}")
|
| 1050 | + |
| 1051 | + |
| 1052 | +@pytest.mark.asyncio |
| 1053 | +async def test_bedrock_guardrail_200_with_exception_in_output_raises_and_logs_failure(): |
| 1054 | + """ |
| 1055 | + When Bedrock returns HTTP 200 but the body contains Output.__type with 'Exception', |
| 1056 | + the guardrail should: |
| 1057 | + - raise an HTTPException(400) with the Output payload in detail |
| 1058 | + - log the request trace with guardrail_status='failure' |
| 1059 | + """ |
| 1060 | + guardrail = BedrockGuardrail( |
| 1061 | + guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT" |
| 1062 | + ) |
| 1063 | + |
| 1064 | + # Mock a Bedrock "success" HTTP status but an Exception embedded in the body |
| 1065 | + payload = { |
| 1066 | + "Output": { |
| 1067 | + "__type": "com.amazonaws#InternalServerException", |
| 1068 | + "message": "Something went wrong upstream", |
| 1069 | + }, |
| 1070 | + "action": "NONE", |
| 1071 | + } |
| 1072 | + mock_resp = MagicMock() |
| 1073 | + mock_resp.status_code = 200 |
| 1074 | + mock_resp.content = json.dumps(payload).encode("utf-8") |
| 1075 | + mock_resp.text = json.dumps(payload) |
| 1076 | + mock_resp.json.return_value = payload |
| 1077 | + |
| 1078 | + # Minimal request data |
| 1079 | + request_data = { |
| 1080 | + "model": "gpt-4o", |
| 1081 | + "messages": [{"role": "user", "content": "hello"}], |
| 1082 | + } |
| 1083 | + |
| 1084 | + # Mock creds and request prep |
| 1085 | + mock_credentials = MagicMock() |
| 1086 | + mock_credentials.access_key = "ak" |
| 1087 | + mock_credentials.secret_key = "sk" |
| 1088 | + mock_credentials.token = None |
| 1089 | + |
| 1090 | + with patch.object( |
| 1091 | + guardrail.async_handler, "post", new_callable=AsyncMock |
| 1092 | + ) as mock_post, patch.object( |
| 1093 | + guardrail, "_load_credentials", return_value=(mock_credentials, "us-east-1") |
| 1094 | + ), patch.object( |
| 1095 | + guardrail, |
| 1096 | + "_prepare_request", |
| 1097 | + return_value=MagicMock(url="http://example", headers={}, body=b""), |
| 1098 | + ), patch.object( |
| 1099 | + guardrail, "add_standard_logging_guardrail_information_to_request_data" |
| 1100 | + ) as mock_add_trace: |
| 1101 | + mock_post.return_value = mock_resp |
| 1102 | + |
| 1103 | + with pytest.raises(HTTPException) as excinfo: |
| 1104 | + await guardrail.make_bedrock_api_request( |
| 1105 | + source="INPUT", |
| 1106 | + messages=request_data["messages"], |
| 1107 | + request_data=request_data, |
| 1108 | + ) |
| 1109 | + |
| 1110 | + # 1) Raised HTTPException with 400 status |
| 1111 | + err = excinfo.value |
| 1112 | + assert err.status_code == 400 |
| 1113 | + assert err.detail["error"] == "Guardrail application failed." |
| 1114 | + |
| 1115 | + # 2) Detail includes the Output object from the Bedrock body |
| 1116 | + assert err.detail["bedrock_guardrail_response"] == payload["Output"] |
| 1117 | + |
| 1118 | + # 3) Trace logging received a 'failure' status |
| 1119 | + assert mock_add_trace.called |
| 1120 | + _, kwargs = mock_add_trace.call_args |
| 1121 | + assert kwargs["guardrail_status"] == "failure" |
| 1122 | + # And the JSON passed to tracing is the same response we received |
| 1123 | + assert kwargs["guardrail_json_response"] == payload |
0 commit comments