From 2688582d76595ee6b0815860885aaa7006507911 Mon Sep 17 00:00:00 2001 From: Yuxuan Chen Date: Thu, 23 Apr 2026 15:58:29 -0400 Subject: [PATCH 1/5] Add conftest fixture to transcribe-streaming integration tests --- .../tests/integration/conftest.py | 117 ++++++++++++++++++ .../tests/integration/test_non_streaming.py | 24 +--- .../tests/setup_resources.py | 97 --------------- 3 files changed, 123 insertions(+), 115 deletions(-) create mode 100644 clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py delete mode 100644 clients/aws-sdk-transcribe-streaming/tests/setup_resources.py diff --git a/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py b/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py new file mode 100644 index 0000000..3526ad0 --- /dev/null +++ b/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py @@ -0,0 +1,117 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Pytest fixtures for Transcribe Streaming integration tests. + +Creates and tears down an IAM role and S3 bucket needed for medical scribe +integration tests once per test session. The ``healthscribe_resources`` +fixture provides the role ARN and bucket name. +""" + +import json +import time +import uuid +from typing import Any + +import boto3 +import pytest + +REGION = "us-east-1" +_UNIQUE_SUFFIX = uuid.uuid4().hex +ROLE_NAME = f"HealthScribeIntegTestRole-{_UNIQUE_SUFFIX}" +BUCKET_NAME = f"healthscribe-integ-test-{_UNIQUE_SUFFIX}" + + +def _create_iam_role(iam_client: Any, role_name: str, bucket_name: str) -> None: + """Create an IAM role with S3 PutObject access for Transcribe Streaming.""" + trust_policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": ["transcribe.streaming.amazonaws.com"]}, + "Action": "sts:AssumeRole", + } + ], + } + + try: + iam_client.create_role( + RoleName=role_name, AssumeRolePolicyDocument=json.dumps(trust_policy) + ) + except iam_client.exceptions.EntityAlreadyExistsException: + pass + + permissions_policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Action": ["s3:PutObject"], + "Resource": [ + f"arn:aws:s3:::{bucket_name}", + f"arn:aws:s3:::{bucket_name}/*", + ], + "Effect": "Allow", + } + ], + } + + iam_client.put_role_policy( + RoleName=role_name, + PolicyName="HealthScribeS3Access", + PolicyDocument=json.dumps(permissions_policy), + ) + + +def _create_healthscribe_resources() -> tuple[str, str]: + """Create an IAM role and S3 bucket for medical scribe tests. + + Returns: + Tuple of (role_arn, bucket_name). + """ + iam = boto3.client("iam") + s3 = boto3.client("s3", region_name=REGION) + sts = boto3.client("sts") + + account_id = sts.get_caller_identity()["Account"] + + s3.create_bucket(Bucket=BUCKET_NAME) + _create_iam_role(iam, ROLE_NAME, BUCKET_NAME) + + # Wait for IAM role to propagate across services. + time.sleep(10) + + role_arn = f"arn:aws:iam::{account_id}:role/{ROLE_NAME}" + return role_arn, BUCKET_NAME + + +def _delete_healthscribe_resources() -> None: + """Delete the IAM role and S3 bucket created for tests.""" + iam = boto3.client("iam") + + # Delete bucket and all its objects + bucket = boto3.resource("s3").Bucket(BUCKET_NAME) + try: + bucket.objects.all().delete() + bucket.delete() + except bucket.meta.client.exceptions.NoSuchBucket: + pass + + # Delete inline policy then role + try: + iam.delete_role_policy(RoleName=ROLE_NAME, PolicyName="HealthScribeS3Access") + except iam.exceptions.NoSuchEntityException: + pass + + try: + iam.delete_role(RoleName=ROLE_NAME) + except iam.exceptions.NoSuchEntityException: + pass + + +@pytest.fixture(scope="session") +def healthscribe_resources(): + """Create HealthScribe resources for the test session and delete them after.""" + role_arn, bucket_name = _create_healthscribe_resources() + yield role_arn, bucket_name + _delete_healthscribe_resources() diff --git a/clients/aws-sdk-transcribe-streaming/tests/integration/test_non_streaming.py b/clients/aws-sdk-transcribe-streaming/tests/integration/test_non_streaming.py index 80137bd..7480fc9 100644 --- a/clients/aws-sdk-transcribe-streaming/tests/integration/test_non_streaming.py +++ b/clients/aws-sdk-transcribe-streaming/tests/integration/test_non_streaming.py @@ -1,23 +1,12 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -"""Test non-streaming output type handling. - -This test requires AWS resources (an IAM role and an S3 bucket). -To set them up locally, run: - - uv run scripts/setup_resources.py - -Then export the environment variables shown in the output. -""" +"""Test non-streaming output type handling using Medical Scribe.""" import asyncio -import os import time import uuid -import pytest - from aws_sdk_transcribe_streaming.models import ( ClinicalNoteGenerationSettings, GetMedicalScribeStreamInput, @@ -43,12 +32,11 @@ CHUNK_SIZE = 1024 * 8 -async def test_get_medical_scribe_stream() -> None: - role_arn = os.environ.get("HEALTHSCRIBE_ROLE_ARN") - s3_bucket = os.environ.get("HEALTHSCRIBE_S3_BUCKET") - - if not role_arn or not s3_bucket: - pytest.fail("HEALTHSCRIBE_ROLE_ARN or HEALTHSCRIBE_S3_BUCKET not set") +async def test_get_medical_scribe_stream( + healthscribe_resources: tuple[str, str], +) -> None: + """Test non-streaming GetMedicalScribeStream operation.""" + role_arn, s3_bucket = healthscribe_resources transcribe_client = create_transcribe_client("us-east-1") session_id = str(uuid.uuid4()) diff --git a/clients/aws-sdk-transcribe-streaming/tests/setup_resources.py b/clients/aws-sdk-transcribe-streaming/tests/setup_resources.py deleted file mode 100644 index 35c05f7..0000000 --- a/clients/aws-sdk-transcribe-streaming/tests/setup_resources.py +++ /dev/null @@ -1,97 +0,0 @@ -# /// script -# requires-python = ">=3.12" -# dependencies = [ -# "boto3", -# ] -# /// -# -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Setup script to create AWS resources for integration tests. - -Creates an IAM role and S3 bucket needed for medical scribe integration tests. - -Note: - This script is intended for local testing only and should not be used for - production setups. - -Usage: - uv run scripts/setup_resources.py -""" - -import json -from typing import Any - -import boto3 - - -def create_iam_role(iam_client: Any, role_name: str, bucket_name: str) -> None: - trust_policy = { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": [ - "transcribe.streaming.amazonaws.com" - ] - }, - "Action": "sts:AssumeRole", - } - ] - } - - try: - iam_client.create_role( - RoleName=role_name, AssumeRolePolicyDocument=json.dumps(trust_policy) - ) - except iam_client.exceptions.EntityAlreadyExistsException: - pass - - permissions_policy = { - "Version": "2012-10-17", - "Statement": [ - { - "Action": [ - "s3:PutObject" - ], - "Resource": [ - f"arn:aws:s3:::{bucket_name}", - f"arn:aws:s3:::{bucket_name}/*", - ], - "Effect": "Allow" - } - ] - } - - iam_client.put_role_policy( - RoleName=role_name, - PolicyName="HealthScribeS3Access", - PolicyDocument=json.dumps(permissions_policy), - ) - - -def setup_healthscribe_resources() -> tuple[str, str]: - region = "us-east-1" - iam = boto3.client("iam") - s3 = boto3.client("s3", region_name=region) - sts = boto3.client("sts") - - account_id = sts.get_caller_identity()["Account"] - bucket_name = f"healthscribe-test-{account_id}-{region}" - role_name = "HealthScribeIntegrationTestRole" - - s3.create_bucket(Bucket=bucket_name) - create_iam_role(iam, role_name, bucket_name) - - role_arn = f"arn:aws:iam::{account_id}:role/{role_name}" - return role_arn, bucket_name - - -if __name__ == "__main__": - role_arn, bucket_name = setup_healthscribe_resources() - - print("Setup complete. Export these environment variables before running tests:") - print(f"export HEALTHSCRIBE_ROLE_ARN={role_arn}") - print(f"export HEALTHSCRIBE_S3_BUCKET={bucket_name}") From 304d3866c28e74172bce5d264943e425df7b8961 Mon Sep 17 00:00:00 2001 From: Yuxuan Chen Date: Fri, 1 May 2026 16:06:37 -0400 Subject: [PATCH 2/5] Ensure fixture cleanup on partial setup failure --- .../tests/integration/conftest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py b/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py index 3526ad0..2a36745 100644 --- a/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py +++ b/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py @@ -112,6 +112,8 @@ def _delete_healthscribe_resources() -> None: @pytest.fixture(scope="session") def healthscribe_resources(): """Create HealthScribe resources for the test session and delete them after.""" - role_arn, bucket_name = _create_healthscribe_resources() - yield role_arn, bucket_name - _delete_healthscribe_resources() + try: + role_arn, bucket_name = _create_healthscribe_resources() + yield role_arn, bucket_name + finally: + _delete_healthscribe_resources() From be73af5494871fc68315ae6bab5eb2289e91ff19 Mon Sep 17 00:00:00 2001 From: Yuxuan Chen Date: Fri, 8 May 2026 17:00:51 -0400 Subject: [PATCH 3/5] Address reviewer feedback regarding conftest and IAM role propagation --- .../tests/integration/conftest.py | 95 ++++++++++++------- .../tests/integration/test_non_streaming.py | 32 ++++++- 2 files changed, 89 insertions(+), 38 deletions(-) diff --git a/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py b/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py index 2a36745..ccc449d 100644 --- a/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py +++ b/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py @@ -9,7 +9,6 @@ """ import json -import time import uuid from typing import Any @@ -17,13 +16,16 @@ import pytest REGION = "us-east-1" -_UNIQUE_SUFFIX = uuid.uuid4().hex -ROLE_NAME = f"HealthScribeIntegTestRole-{_UNIQUE_SUFFIX}" -BUCKET_NAME = f"healthscribe-integ-test-{_UNIQUE_SUFFIX}" def _create_iam_role(iam_client: Any, role_name: str, bucket_name: str) -> None: - """Create an IAM role with S3 PutObject access for Transcribe Streaming.""" + """Create an IAM role with S3 PutObject access for Transcribe Streaming. + + Args: + iam_client: A boto3 IAM client. + role_name: The name of the IAM role to create. + bucket_name: The name of the S3 bucket the role is allowed to write to. + """ trust_policy = { "Version": "2012-10-17", "Statement": [ @@ -63,57 +65,84 @@ def _create_iam_role(iam_client: Any, role_name: str, bucket_name: str) -> None: ) -def _create_healthscribe_resources() -> tuple[str, str]: +def _create_healthscribe_resources( + iam_client: Any, s3_client: Any, sts_client: Any, role_name: str, bucket_name: str +) -> str: """Create an IAM role and S3 bucket for medical scribe tests. + Args: + iam_client: A boto3 IAM client. + s3_client: A boto3 S3 client. + sts_client: A boto3 STS client. + role_name: The name of the IAM role to create. + bucket_name: The name of the S3 bucket to create. + Returns: - Tuple of (role_arn, bucket_name). + The IAM role ARN. """ - iam = boto3.client("iam") - s3 = boto3.client("s3", region_name=REGION) - sts = boto3.client("sts") - - account_id = sts.get_caller_identity()["Account"] - - s3.create_bucket(Bucket=BUCKET_NAME) - _create_iam_role(iam, ROLE_NAME, BUCKET_NAME) + account_id = sts_client.get_caller_identity()["Account"] - # Wait for IAM role to propagate across services. - time.sleep(10) + s3_client.create_bucket(Bucket=bucket_name) + _create_iam_role(iam_client, role_name, bucket_name) - role_arn = f"arn:aws:iam::{account_id}:role/{ROLE_NAME}" - return role_arn, BUCKET_NAME + return f"arn:aws:iam::{account_id}:role/{role_name}" -def _delete_healthscribe_resources() -> None: - """Delete the IAM role and S3 bucket created for tests.""" - iam = boto3.client("iam") +def _delete_healthscribe_resources( + iam_client: Any, s3_client: Any, role_name: str, bucket_name: str +) -> None: + """Delete the IAM role and S3 bucket created for tests. - # Delete bucket and all its objects - bucket = boto3.resource("s3").Bucket(BUCKET_NAME) + Args: + iam_client: A boto3 IAM client. + s3_client: A boto3 S3 client. + role_name: The name of the IAM role to delete. + bucket_name: The name of the S3 bucket to delete. + """ + # Empty and delete the bucket try: - bucket.objects.all().delete() - bucket.delete() - except bucket.meta.client.exceptions.NoSuchBucket: + paginator = s3_client.get_paginator("list_objects_v2") + for page in paginator.paginate(Bucket=bucket_name): + objects = page.get("Contents") + if not objects: + continue + s3_client.delete_objects( + Bucket=bucket_name, + Delete={"Objects": [{"Key": o["Key"]} for o in objects]}, + ) + s3_client.delete_bucket(Bucket=bucket_name) + except s3_client.exceptions.NoSuchBucket: pass # Delete inline policy then role try: - iam.delete_role_policy(RoleName=ROLE_NAME, PolicyName="HealthScribeS3Access") - except iam.exceptions.NoSuchEntityException: + iam_client.delete_role_policy( + RoleName=role_name, PolicyName="HealthScribeS3Access" + ) + except iam_client.exceptions.NoSuchEntityException: pass try: - iam.delete_role(RoleName=ROLE_NAME) - except iam.exceptions.NoSuchEntityException: + iam_client.delete_role(RoleName=role_name) + except iam_client.exceptions.NoSuchEntityException: pass @pytest.fixture(scope="session") def healthscribe_resources(): """Create HealthScribe resources for the test session and delete them after.""" + unique_suffix = uuid.uuid4().hex + role_name = f"HealthScribeIntegTestRole-{unique_suffix}" + bucket_name = f"healthscribe-integ-test-{unique_suffix}" + + iam_client = boto3.client("iam") + s3_client = boto3.client("s3", region_name=REGION) + sts_client = boto3.client("sts") + try: - role_arn, bucket_name = _create_healthscribe_resources() + role_arn = _create_healthscribe_resources( + iam_client, s3_client, sts_client, role_name, bucket_name + ) yield role_arn, bucket_name finally: - _delete_healthscribe_resources() + _delete_healthscribe_resources(iam_client, s3_client, role_name, bucket_name) diff --git a/clients/aws-sdk-transcribe-streaming/tests/integration/test_non_streaming.py b/clients/aws-sdk-transcribe-streaming/tests/integration/test_non_streaming.py index 7480fc9..a254282 100644 --- a/clients/aws-sdk-transcribe-streaming/tests/integration/test_non_streaming.py +++ b/clients/aws-sdk-transcribe-streaming/tests/integration/test_non_streaming.py @@ -8,6 +8,7 @@ import uuid from aws_sdk_transcribe_streaming.models import ( + BadRequestException, ClinicalNoteGenerationSettings, GetMedicalScribeStreamInput, GetMedicalScribeStreamOutput, @@ -31,13 +32,14 @@ CHANNEL_NUMS = 1 CHUNK_SIZE = 1024 * 8 +# Maximum time to wait for IAM role propagation across services. +ROLE_PROPAGATION_TIMEOUT = 300 +# Delay between retries while waiting for IAM role propagation. +ROLE_PROPAGATION_RETRY_DELAY = 5 -async def test_get_medical_scribe_stream( - healthscribe_resources: tuple[str, str], -) -> None: - """Test non-streaming GetMedicalScribeStream operation.""" - role_arn, s3_bucket = healthscribe_resources +async def _run_medical_scribe_session(role_arn: str, s3_bucket: str) -> None: + """Run a full Medical Scribe streaming session and verify its completion.""" transcribe_client = create_transcribe_client("us-east-1") session_id = str(uuid.uuid4()) @@ -109,3 +111,23 @@ async def test_get_medical_scribe_stream( assert details.language_code == "en-US" assert details.media_encoding == "pcm" assert details.media_sample_rate_hertz == SAMPLE_RATE + + +async def test_get_medical_scribe_stream( + healthscribe_resources: tuple[str, str], +) -> None: + """Test non-streaming GetMedicalScribeStream operation. + + IAM is eventually consistent, so Transcribe may not be able to assume the + newly created role immediately. Retry on BadRequestException until the + role has propagated, or until the timeout is reached. + """ + role_arn, s3_bucket = healthscribe_resources + + async with asyncio.timeout(ROLE_PROPAGATION_TIMEOUT): + while True: + try: + await _run_medical_scribe_session(role_arn, s3_bucket) + return + except BadRequestException: + await asyncio.sleep(ROLE_PROPAGATION_RETRY_DELAY) From 395d0a6ed60c6c022ed8db43b8053c6f3d86c4b7 Mon Sep 17 00:00:00 2001 From: Yuxuan Chen Date: Sat, 9 May 2026 14:10:41 -0400 Subject: [PATCH 4/5] Address reviewer feedback on conftest and retry handling --- .../tests/integration/conftest.py | 9 +++------ .../tests/integration/test_non_streaming.py | 20 ++++++++++++------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py b/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py index ccc449d..0297d56 100644 --- a/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py +++ b/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py @@ -37,12 +37,9 @@ def _create_iam_role(iam_client: Any, role_name: str, bucket_name: str) -> None: ], } - try: - iam_client.create_role( - RoleName=role_name, AssumeRolePolicyDocument=json.dumps(trust_policy) - ) - except iam_client.exceptions.EntityAlreadyExistsException: - pass + iam_client.create_role( + RoleName=role_name, AssumeRolePolicyDocument=json.dumps(trust_policy) + ) permissions_policy = { "Version": "2012-10-17", diff --git a/clients/aws-sdk-transcribe-streaming/tests/integration/test_non_streaming.py b/clients/aws-sdk-transcribe-streaming/tests/integration/test_non_streaming.py index a254282..93c2bf9 100644 --- a/clients/aws-sdk-transcribe-streaming/tests/integration/test_non_streaming.py +++ b/clients/aws-sdk-transcribe-streaming/tests/integration/test_non_streaming.py @@ -124,10 +124,16 @@ async def test_get_medical_scribe_stream( """ role_arn, s3_bucket = healthscribe_resources - async with asyncio.timeout(ROLE_PROPAGATION_TIMEOUT): - while True: - try: - await _run_medical_scribe_session(role_arn, s3_bucket) - return - except BadRequestException: - await asyncio.sleep(ROLE_PROPAGATION_RETRY_DELAY) + last_error: BadRequestException | None = None + try: + async with asyncio.timeout(ROLE_PROPAGATION_TIMEOUT): + while True: + try: + await _run_medical_scribe_session(role_arn, s3_bucket) + return + except BadRequestException as e: + last_error = e + await asyncio.sleep(ROLE_PROPAGATION_RETRY_DELAY) + except TimeoutError: + assert last_error is not None + raise last_error From a87462ac01ba3ef67975db5561c6522ac91ed685 Mon Sep 17 00:00:00 2001 From: Yuxuan Chen Date: Mon, 11 May 2026 16:48:16 -0400 Subject: [PATCH 5/5] Add tags and prefix to integ-test resources for orphan cleanup --- .../tests/integration/conftest.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py b/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py index 0297d56..b771dc1 100644 --- a/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py +++ b/clients/aws-sdk-transcribe-streaming/tests/integration/conftest.py @@ -17,6 +17,10 @@ REGION = "us-east-1" +# Tags applied to all resources so orphaned resources from interrupted +# test runs can be discovered and cleaned up. +_TAGS = [{"Key": "Purpose", "Value": "IntegTest"}] + def _create_iam_role(iam_client: Any, role_name: str, bucket_name: str) -> None: """Create an IAM role with S3 PutObject access for Transcribe Streaming. @@ -38,7 +42,9 @@ def _create_iam_role(iam_client: Any, role_name: str, bucket_name: str) -> None: } iam_client.create_role( - RoleName=role_name, AssumeRolePolicyDocument=json.dumps(trust_policy) + RoleName=role_name, + AssumeRolePolicyDocument=json.dumps(trust_policy), + Tags=_TAGS, ) permissions_policy = { @@ -57,7 +63,7 @@ def _create_iam_role(iam_client: Any, role_name: str, bucket_name: str) -> None: iam_client.put_role_policy( RoleName=role_name, - PolicyName="HealthScribeS3Access", + PolicyName="healthscribe-s3-access", PolicyDocument=json.dumps(permissions_policy), ) @@ -80,6 +86,7 @@ def _create_healthscribe_resources( account_id = sts_client.get_caller_identity()["Account"] s3_client.create_bucket(Bucket=bucket_name) + s3_client.put_bucket_tagging(Bucket=bucket_name, Tagging={"TagSet": _TAGS}) _create_iam_role(iam_client, role_name, bucket_name) return f"arn:aws:iam::{account_id}:role/{role_name}" @@ -114,7 +121,7 @@ def _delete_healthscribe_resources( # Delete inline policy then role try: iam_client.delete_role_policy( - RoleName=role_name, PolicyName="HealthScribeS3Access" + RoleName=role_name, PolicyName="healthscribe-s3-access" ) except iam_client.exceptions.NoSuchEntityException: pass @@ -128,9 +135,10 @@ def _delete_healthscribe_resources( @pytest.fixture(scope="session") def healthscribe_resources(): """Create HealthScribe resources for the test session and delete them after.""" - unique_suffix = uuid.uuid4().hex - role_name = f"HealthScribeIntegTestRole-{unique_suffix}" - bucket_name = f"healthscribe-integ-test-{unique_suffix}" + # Shortened UUID to keep IAM role name under the 64-character limit. + unique_suffix = uuid.uuid4().hex[:16] + role_name = f"integ-test-transcribe-streaming-role-{unique_suffix}" + bucket_name = f"integ-test-transcribe-streaming-bucket-{unique_suffix}" iam_client = boto3.client("iam") s3_client = boto3.client("s3", region_name=REGION)