-
Notifications
You must be signed in to change notification settings - Fork 147
feat(gateway): add create_web_search_target() helper #656
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| """Tests for GatewayClient Web Search target helper methods.""" | ||
|
|
||
| from unittest.mock import MagicMock, Mock | ||
|
|
||
| from bedrock_agentcore.gateway.client import GatewayClient | ||
|
|
||
|
|
||
| class TestCreateWebSearchTarget: | ||
| """Tests for create_web_search_target.""" | ||
|
|
||
| def _make_client(self): | ||
| mock_session = MagicMock() | ||
| mock_session.region_name = "us-west-2" | ||
| client = GatewayClient(boto3_session=mock_session) | ||
| client.create_gateway_target_and_wait = Mock(return_value={"status": "READY", "targetId": "t-789"}) | ||
| return client | ||
|
|
||
| def test_minimal(self): | ||
| client = self._make_client() | ||
|
|
||
| result = client.create_web_search_target(gateway_identifier="gw-123") | ||
|
|
||
| assert result["status"] == "READY" | ||
| client.create_gateway_target_and_wait.assert_called_once_with( | ||
| wait_config=None, | ||
| gatewayIdentifier="gw-123", | ||
| name="amazon-web-search", | ||
| targetConfiguration={ | ||
| "mcp": { | ||
| "connector": { | ||
| "source": {"connectorId": "web-search"}, | ||
| "enabled": ["WebSearch"], | ||
| "configurations": [{"name": "WebSearch", "parameterValues": {}}], | ||
| }, | ||
| }, | ||
| }, | ||
| credentialProviderConfigurations=[ | ||
| {"credentialProviderType": "GATEWAY_IAM_ROLE"}, | ||
| ], | ||
| ) | ||
|
|
||
| def test_with_all_options(self): | ||
| client = self._make_client() | ||
|
|
||
| result = client.create_web_search_target( | ||
| gateway_identifier="gw-123", | ||
| name="custom-search", | ||
| description="Search the public web", | ||
| exclude_domains=["example.com", "spam.example"], | ||
| include_domains=["allowed.example"], | ||
| connector_version="1.2.0", | ||
| parameter_overrides=[{"path": "$.maxResults", "visible": True}], | ||
| ) | ||
|
|
||
| assert result["status"] == "READY" | ||
| call_kwargs = client.create_gateway_target_and_wait.call_args[1] | ||
| assert call_kwargs["name"] == "custom-search" | ||
| connector = call_kwargs["targetConfiguration"]["mcp"]["connector"] | ||
| assert connector["source"] == {"connectorId": "web-search", "version": "1.2.0"} | ||
| assert connector["enabled"] == ["WebSearch"] | ||
| config = connector["configurations"][0] | ||
| assert config["name"] == "WebSearch" | ||
| assert config["description"] == "Search the public web" | ||
| assert config["parameterValues"] == { | ||
| "domainFilter": { | ||
| "include": ["allowed.example"], | ||
| "exclude": ["example.com", "spam.example"], | ||
| } | ||
| } | ||
| assert config["parameterOverrides"] == [{"path": "$.maxResults", "visible": True}] | ||
|
|
||
| def test_include_domains_only(self): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This asserts a request the service will reject (see comment on |
||
| client = self._make_client() | ||
|
|
||
| client.create_web_search_target( | ||
| gateway_identifier="gw-123", | ||
| include_domains=["docs.aws.amazon.com"], | ||
| ) | ||
|
|
||
| call_kwargs = client.create_gateway_target_and_wait.call_args[1] | ||
| config = call_kwargs["targetConfiguration"]["mcp"]["connector"]["configurations"][0] | ||
| assert config["parameterValues"] == {"domainFilter": {"include": ["docs.aws.amazon.com"]}} | ||
|
|
||
| def test_connector_version_omitted_by_default(self): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| client = self._make_client() | ||
|
|
||
| client.create_web_search_target(gateway_identifier="gw-123") | ||
|
|
||
| call_kwargs = client.create_gateway_target_and_wait.call_args[1] | ||
| source = call_kwargs["targetConfiguration"]["mcp"]["connector"]["source"] | ||
| assert source == {"connectorId": "web-search"} | ||
|
|
||
| def test_no_domain_filter_when_no_exclude_domains(self): | ||
| client = self._make_client() | ||
|
|
||
| client.create_web_search_target(gateway_identifier="gw-123") | ||
|
|
||
| call_kwargs = client.create_gateway_target_and_wait.call_args[1] | ||
| config = call_kwargs["targetConfiguration"]["mcp"]["connector"]["configurations"][0] | ||
| assert config["parameterValues"] == {} | ||
|
|
||
| def test_empty_exclude_domains_is_omitted(self): | ||
| client = self._make_client() | ||
|
|
||
| client.create_web_search_target(gateway_identifier="gw-123", exclude_domains=[]) | ||
|
|
||
| call_kwargs = client.create_gateway_target_and_wait.call_args[1] | ||
| config = call_kwargs["targetConfiguration"]["mcp"]["connector"]["configurations"][0] | ||
| assert config["parameterValues"] == {} | ||
|
|
||
| def test_parameter_values_always_present(self): | ||
| """The service drops configurations without parameterValues, then rejects the | ||
| request as empty, so the key is sent even when there is nothing to configure.""" | ||
| client = self._make_client() | ||
|
|
||
| client.create_web_search_target(gateway_identifier="gw-123", description="Search the web") | ||
|
|
||
| call_kwargs = client.create_gateway_target_and_wait.call_args[1] | ||
| config = call_kwargs["targetConfiguration"]["mcp"]["connector"]["configurations"][0] | ||
| assert "parameterValues" in config | ||
|
|
||
| def test_kwargs_override_target_configuration(self): | ||
| client = self._make_client() | ||
|
|
||
| custom_target_config = {"mcp": {"lambda": {"lambdaArn": "arn:..."}}} | ||
| client.create_web_search_target( | ||
| gateway_identifier="gw-123", | ||
| targetConfiguration=custom_target_config, | ||
| ) | ||
|
|
||
| call_kwargs = client.create_gateway_target_and_wait.call_args[1] | ||
| assert call_kwargs["targetConfiguration"] == custom_target_config | ||
|
|
||
| def test_kwargs_override_credential_provider(self): | ||
| client = self._make_client() | ||
|
|
||
| custom_creds = [{"credentialProviderType": "CUSTOM"}] | ||
| client.create_web_search_target( | ||
| gateway_identifier="gw-123", | ||
| credentialProviderConfigurations=custom_creds, | ||
| ) | ||
|
|
||
| call_kwargs = client.create_gateway_target_and_wait.call_args[1] | ||
| assert call_kwargs["credentialProviderConfigurations"] == custom_creds | ||
|
|
||
| def test_default_credential_provider(self): | ||
| client = self._make_client() | ||
|
|
||
| client.create_web_search_target(gateway_identifier="gw-123") | ||
|
|
||
| call_kwargs = client.create_gateway_target_and_wait.call_args[1] | ||
| assert call_kwargs["credentialProviderConfigurations"] == [ | ||
| {"credentialProviderType": "GATEWAY_IAM_ROLE"}, | ||
| ] | ||
|
|
||
| def test_wait_config_passed_through(self): | ||
| from bedrock_agentcore._utils.config import WaitConfig | ||
|
|
||
| client = self._make_client() | ||
| wc = WaitConfig(max_wait=60, poll_interval=5) | ||
|
|
||
| client.create_web_search_target(gateway_identifier="gw-123", wait_config=wc) | ||
|
|
||
| assert client.create_gateway_target_and_wait.call_args[1]["wait_config"] == wc | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| """Integration tests for GatewayClient Web Search target helper methods. | ||
|
|
||
| Requires environment variables: | ||
| BEDROCK_TEST_REGION: AWS region (default: us-east-1). The web-search connector is | ||
| only offered in us-east-1, eu-west-1 and ap-northeast-1. | ||
| GATEWAY_ROLE_ARN: IAM role ARN with AgentCore gateway trust policy | ||
| """ | ||
|
|
||
| import os | ||
| import time | ||
|
|
||
| import pytest | ||
| from botocore.exceptions import ClientError | ||
|
|
||
| from bedrock_agentcore.gateway.client import GatewayClient | ||
|
|
||
|
|
||
| @pytest.mark.integration | ||
| class TestGatewayWebSearchTarget: | ||
| """Integration tests for create_web_search_target.""" | ||
|
|
||
| @classmethod | ||
| def setup_class(cls): | ||
| cls.region = os.environ.get("BEDROCK_TEST_REGION", "us-east-1") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Every other gateway integ test defaults to |
||
| cls.gateway_role_arn = os.environ.get("GATEWAY_ROLE_ARN") | ||
| if not cls.gateway_role_arn: | ||
| pytest.fail("GATEWAY_ROLE_ARN must be set") | ||
|
|
||
| cls.gateway_client = GatewayClient(region_name=cls.region) | ||
| cls.test_prefix = f"sdk-integ-ws-tgt-{int(time.time())}" | ||
| cls.gateway_id = None | ||
| cls.target_ids = [] | ||
|
|
||
| gw = cls.gateway_client.create_gateway_and_wait( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the connector is not available, all three tests skip but |
||
| name=f"{cls.test_prefix}-gw", | ||
| roleArn=cls.gateway_role_arn, | ||
| authorizerType="NONE", | ||
| protocolType="MCP", | ||
| ) | ||
| cls.gateway_id = gw["gatewayId"] | ||
|
|
||
| @classmethod | ||
| def teardown_class(cls): | ||
| for target_id in cls.target_ids: | ||
| try: | ||
| cls.gateway_client.delete_gateway_target_and_wait( | ||
| gatewayIdentifier=cls.gateway_id, | ||
| targetId=target_id, | ||
| ) | ||
| except Exception as e: | ||
| print(f"Failed to delete target {target_id}: {e}") | ||
|
|
||
| if cls.gateway_id: | ||
| try: | ||
| cls.gateway_client.delete_gateway_and_wait(gatewayIdentifier=cls.gateway_id) | ||
| except Exception as e: | ||
| print(f"Failed to delete gateway {cls.gateway_id}: {e}") | ||
|
|
||
| def _create_target(self, **kwargs): | ||
| """Create a web search target, skipping the test if the account is not entitled. | ||
|
|
||
| The web-search connector is enabled per account. When it is not, CreateGatewayTarget | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This says the connector is enabled per account, but the module docstring says it is only offered in three regions. If the skip in CI is really a region mismatch, a reader will look at the wrong thing. Worth reconciling the two explanations. |
||
| rejects the request with "Connector integration web-search is not available for this | ||
| account." Any other error still fails the test. | ||
| """ | ||
| try: | ||
| return self.gateway_client.create_web_search_target(gateway_identifier=self.gateway_id, **kwargs) | ||
| except ClientError as e: | ||
| error = e.response.get("Error", {}) | ||
| if error.get("Code") == "ValidationException" and "not available for this account" in error.get( | ||
| "Message", "" | ||
| ): | ||
| pytest.skip(f"web-search connector not enabled for this account: {error.get('Message')}") | ||
| raise | ||
|
|
||
| @pytest.mark.order(1) | ||
| def test_create_web_search_target_minimal(self): | ||
| target = self._create_target() | ||
| self.__class__.target_ids.append(target["targetId"]) | ||
| assert target["status"] == "READY" | ||
| assert target["name"] == "amazon-web-search" | ||
|
|
||
| @pytest.mark.order(2) | ||
| def test_create_web_search_target_with_options(self): | ||
| target = self._create_target( | ||
| name=f"{self.test_prefix}-custom", | ||
| description="Search the public web", | ||
| exclude_domains=["example.com"], | ||
| include_domains=["docs.aws.amazon.com"], | ||
| connector_version="1.2.0", | ||
| parameter_overrides=[{"path": "$.maxResults", "visible": True, "description": "How many results"}], | ||
| ) | ||
| self.__class__.target_ids.append(target["targetId"]) | ||
| assert target["status"] == "READY" | ||
| assert target["name"] == f"{self.test_prefix}-custom" | ||
|
|
||
| @pytest.mark.order(3) | ||
| def test_create_web_search_target_with_credential_config(self): | ||
| target = self._create_target( | ||
| name=f"{self.test_prefix}-cred", | ||
| credentialProviderConfigurations=[ | ||
| {"credentialProviderType": "GATEWAY_IAM_ROLE"}, | ||
| ], | ||
| ) | ||
| self.__class__.target_ids.append(target["targetId"]) | ||
| assert target["status"] == "READY" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
include_domainsis set andconnector_versionis not, this sendsdomainFilter.includeagainst the connector default (1.1.0), which only 1.2.0+ accepts. The service rejects it with a ValidationException. Since the point of the helper is to hide connector details, either defaultsource["version"]to"1.2.0"wheninclude_domainsis set, or raise aValueErrorhere so the caller gets a clear message instead of a server-side error.