Skip to content

Commit 042edf1

Browse files
authored
feat: Add contract tests for new CFN Primary Id attribute and AWS resources (#283)
*Description of changes:* Updating our contract tests to verify the new cloudformation primary id attribute. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 095c109 commit 042edf1

File tree

5 files changed

+368
-4
lines changed

5 files changed

+368
-4
lines changed

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,16 @@ def patch_extract_attributes(self, attributes: _AttributeMapT):
178178
if queue_url:
179179
attributes[AWS_SQS_QUEUE_URL] = queue_url
180180

181+
old_on_success = _SqsExtension.on_success
182+
183+
def patch_on_success(self, span: Span, result: _BotoResultT):
184+
old_on_success(self, span, result)
185+
queue_url = result.get("QueueUrl")
186+
if queue_url:
187+
span.set_attribute(AWS_SQS_QUEUE_URL, queue_url)
188+
181189
_SqsExtension.extract_attributes = patch_extract_attributes
190+
_SqsExtension.on_success = patch_on_success
182191

183192

184193
def _apply_botocore_bedrock_patch() -> None:

contract-tests/images/applications/botocore/botocore_server.py

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import requests
1313
from botocore.client import BaseClient
1414
from botocore.config import Config
15+
from botocore.exceptions import ClientError
1516
from typing_extensions import Tuple, override
1617

1718
_PORT: int = 8080
@@ -45,6 +46,10 @@ def do_GET(self):
4546
self._handle_kinesis_request()
4647
if self.in_path("bedrock"):
4748
self._handle_bedrock_request()
49+
if self.in_path("secretsmanager"):
50+
self._handle_secretsmanager_request()
51+
if self.in_path("stepfunctions"):
52+
self._handle_stepfunctions_request()
4853

4954
self._end_request(self.main_status)
5055

@@ -246,7 +251,11 @@ def _handle_bedrock_request(self) -> None:
246251
set_main_status(200)
247252
bedrock_client.meta.events.register(
248253
"before-call.bedrock.GetGuardrail",
249-
lambda **kwargs: inject_200_success(guardrailId="bt4o77i015cu", **kwargs),
254+
lambda **kwargs: inject_200_success(
255+
guardrailId="bt4o77i015cu",
256+
guardrailArn="arn:aws:bedrock:us-east-1:000000000000:guardrail/bt4o77i015cu",
257+
**kwargs,
258+
),
250259
)
251260
bedrock_client.get_guardrail(
252261
guardrailIdentifier="arn:aws:bedrock:us-east-1:000000000000:guardrail/bt4o77i015cu"
@@ -301,6 +310,69 @@ def _handle_bedrock_request(self) -> None:
301310
else:
302311
set_main_status(404)
303312

313+
def _handle_secretsmanager_request(self) -> None:
314+
secretsmanager_client = boto3.client("secretsmanager", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION)
315+
if self.in_path(_ERROR):
316+
set_main_status(400)
317+
try:
318+
error_client = boto3.client("secretsmanager", endpoint_url=_ERROR_ENDPOINT, region_name=_AWS_REGION)
319+
error_client.describe_secret(
320+
SecretId="arn:aws:secretsmanager:us-west-2:000000000000:secret:unExistSecret"
321+
)
322+
except Exception as exception:
323+
print("Expected exception occurred", exception)
324+
elif self.in_path(_FAULT):
325+
set_main_status(500)
326+
try:
327+
fault_client = boto3.client(
328+
"secretsmanager", endpoint_url=_FAULT_ENDPOINT, region_name=_AWS_REGION, config=_NO_RETRY_CONFIG
329+
)
330+
fault_client.get_secret_value(
331+
SecretId="arn:aws:secretsmanager:us-west-2:000000000000:secret:nonexistent-secret"
332+
)
333+
except Exception as exception:
334+
print("Expected exception occurred", exception)
335+
elif self.in_path("describesecret/my-secret"):
336+
set_main_status(200)
337+
secretsmanager_client.describe_secret(SecretId="testSecret")
338+
else:
339+
set_main_status(404)
340+
341+
def _handle_stepfunctions_request(self) -> None:
342+
sfn_client = boto3.client("stepfunctions", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION)
343+
if self.in_path(_ERROR):
344+
set_main_status(400)
345+
try:
346+
error_client = boto3.client("stepfunctions", endpoint_url=_ERROR_ENDPOINT, region_name=_AWS_REGION)
347+
error_client.describe_state_machine(
348+
stateMachineArn="arn:aws:states:us-west-2:000000000000:stateMachine:unExistStateMachine"
349+
)
350+
except Exception as exception:
351+
print("Expected exception occurred", exception)
352+
elif self.in_path(_FAULT):
353+
set_main_status(500)
354+
try:
355+
fault_client = boto3.client("stepfunctions", endpoint_url=_FAULT_ENDPOINT, region_name=_AWS_REGION)
356+
fault_client.meta.events.register(
357+
"before-call.stepfunctions.ListStateMachineVersions",
358+
lambda **kwargs: inject_500_error("ListStateMachineVersions", **kwargs),
359+
)
360+
fault_client.list_state_machine_versions(
361+
stateMachineArn="arn:aws:states:us-west-2:000000000000:stateMachine:invalid-state-machine",
362+
)
363+
except Exception as exception:
364+
print("Expected exception occurred", exception)
365+
elif self.in_path("describestatemachine/my-state-machine"):
366+
set_main_status(200)
367+
sfn_client.describe_state_machine(
368+
stateMachineArn="arn:aws:states:us-west-2:000000000000:stateMachine:testStateMachine"
369+
)
370+
elif self.in_path("describeactivity/my-activity"):
371+
set_main_status(200)
372+
sfn_client.describe_activity(activityArn="arn:aws:states:us-west-2:000000000000:activity:testActivity")
373+
else:
374+
set_main_status(404)
375+
304376
def _end_request(self, status_code: int):
305377
self.send_response_only(status_code)
306378
self.end_headers()
@@ -310,6 +382,7 @@ def set_main_status(status: int) -> None:
310382
RequestHandler.main_status = status
311383

312384

385+
# pylint: disable=too-many-locals
313386
def prepare_aws_server() -> None:
314387
requests.Request(method="POST", url="http://localhost:4566/_localstack/state/reset")
315388
try:
@@ -345,6 +418,57 @@ def prepare_aws_server() -> None:
345418
# Set up Kinesis so tests can access a stream.
346419
kinesis_client: BaseClient = boto3.client("kinesis", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION)
347420
kinesis_client.create_stream(StreamName="test_stream", ShardCount=1)
421+
422+
# Set up Secrets Manager so tests can access a secret.
423+
secretsmanager_client: BaseClient = boto3.client(
424+
"secretsmanager", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION
425+
)
426+
secretsmanager_response = secretsmanager_client.list_secrets()
427+
secret = next((s for s in secretsmanager_response["SecretList"] if s["Name"] == "testSecret"), None)
428+
if not secret:
429+
secretsmanager_client.create_secret(
430+
Name="testSecret", SecretString="secretValue", Description="This is a test secret"
431+
)
432+
433+
# Set up Step Functions so tests can access a state machine and activity.
434+
sfn_client: BaseClient = boto3.client("stepfunctions", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION)
435+
sfn_response = sfn_client.list_state_machines()
436+
state_machine_name = "testStateMachine"
437+
activity_name = "testActivity"
438+
state_machine = next((st for st in sfn_response["stateMachines"] if st["name"] == state_machine_name), None)
439+
if not state_machine:
440+
# create state machine needs an iam role so we create it here
441+
iam_client: BaseClient = boto3.client("iam", endpoint_url=_AWS_SDK_ENDPOINT, region_name=_AWS_REGION)
442+
iam_role_name = "testRole"
443+
iam_role_arn = None
444+
trust_policy = {
445+
"Version": "2012-10-17",
446+
"Statement": [
447+
{"Effect": "Allow", "Principal": {"Service": "states.amazonaws.com"}, "Action": "sts:AssumeRole"}
448+
],
449+
}
450+
try:
451+
iam_response = iam_client.create_role(
452+
RoleName=iam_role_name, AssumeRolePolicyDocument=json.dumps(trust_policy)
453+
)
454+
iam_client.attach_role_policy(
455+
RoleName=iam_role_name, PolicyArn="arn:aws:iam::aws:policy/AWSStepFunctionsFullAccess"
456+
)
457+
print(f"IAM Role '{iam_role_name}' create successfully.")
458+
iam_role_arn = iam_response["Role"]["Arn"]
459+
sfn_defintion = {
460+
"Comment": "A simple sequential workflow",
461+
"StartAt": "FirstState",
462+
"States": {"FirstState": {"Type": "Pass", "Result": "Hello, World!", "End": True}},
463+
}
464+
definition_string = json.dumps(sfn_defintion)
465+
sfn_client.create_state_machine(
466+
name=state_machine_name, definition=definition_string, roleArn=iam_role_arn
467+
)
468+
sfn_client.create_activity(name=activity_name)
469+
except Exception as exception:
470+
print("Something went wrong with Step Functions setup", exception)
471+
348472
except Exception as exception:
349473
print("Unexpected exception occurred", exception)
350474

@@ -363,6 +487,9 @@ def inject_200_success(**kwargs):
363487
guardrail_id = kwargs.get("guardrailId")
364488
if guardrail_id is not None:
365489
response_body["guardrailId"] = guardrail_id
490+
guardrail_arn = kwargs.get("guardrailArn")
491+
if guardrail_arn is not None:
492+
response_body["guardrailArn"] = guardrail_arn
366493

367494
HTTPResponse = namedtuple("HTTPResponse", ["status_code", "headers", "body"])
368495
headers = kwargs.get("headers", {})
@@ -371,6 +498,16 @@ def inject_200_success(**kwargs):
371498
return http_response, response_body
372499

373500

501+
def inject_500_error(api_name: str, **kwargs):
502+
raise ClientError(
503+
{
504+
"Error": {"Code": "InternalServerError", "Message": "Internal Server Error"},
505+
"ResponseMetadata": {"HTTPStatusCode": 500, "RequestId": "mock-request-id"},
506+
},
507+
api_name,
508+
)
509+
510+
374511
def main() -> None:
375512
prepare_aws_server()
376513
server_address: Tuple[str, int] = ("0.0.0.0", _PORT)

contract-tests/tests/test/amazon/base/contract_test_base.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
3+
import re
34
import time
45
from logging import INFO, Logger, getLogger
56
from typing import Dict, List
@@ -171,6 +172,12 @@ def _assert_int_attribute(self, attributes_dict: Dict[str, AnyValue], key: str,
171172
self.assertIsNotNone(actual_value)
172173
self.assertEqual(expected_value, actual_value.int_value)
173174

175+
def _assert_match_attribute(self, attributes_dict: Dict[str, AnyValue], key: str, pattern: str) -> None:
176+
self.assertIn(key, attributes_dict)
177+
actual_value: AnyValue = attributes_dict[key]
178+
self.assertIsNotNone(actual_value)
179+
self.assertRegex(actual_value.string_value, pattern)
180+
174181
def check_sum(self, metric_name: str, actual_sum: float, expected_sum: float) -> None:
175182
if metric_name is LATENCY_METRIC:
176183
self.assertTrue(0 < actual_sum < expected_sum)
@@ -221,3 +228,10 @@ def _assert_metric_attributes(
221228
self, resource_scope_metrics: List[ResourceScopeMetric], metric_name: str, expected_sum: int, **kwargs
222229
):
223230
self.fail("Tests must implement this function")
231+
232+
def _is_valid_regex(self, pattern: str) -> bool:
233+
try:
234+
re.compile(pattern)
235+
return True
236+
except re.error:
237+
return False

0 commit comments

Comments
 (0)