• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

localstack / localstack / 21708052316

05 Feb 2026 10:30AM UTC coverage: 86.873% (-0.09%) from 86.962%
21708052316

push

github

web-flow
Deprecate TaggingService (#13697)

2 of 2 new or added lines in 1 file covered. (100.0%)

158 existing lines in 15 files now uncovered.

69932 of 80499 relevant lines covered (86.87%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.34
/localstack-core/localstack/testing/pytest/fixtures.py
1
import contextlib
1✔
2
import dataclasses
1✔
3
import json
1✔
4
import logging
1✔
5
import os
1✔
6
import re
1✔
7
import textwrap
1✔
8
import time
1✔
9
from collections.abc import Callable
1✔
10
from typing import TYPE_CHECKING, Any, Unpack
1✔
11
from unittest.mock import MagicMock
1✔
12

13
import botocore.auth
1✔
14
import botocore.config
1✔
15
import botocore.credentials
1✔
16
import botocore.session
1✔
17
import pytest
1✔
18
from _pytest.config import Config
1✔
19
from _pytest.nodes import Item
1✔
20
from botocore.exceptions import ClientError
1✔
21
from botocore.regions import EndpointResolver
1✔
22
from pytest_httpserver import HTTPServer
1✔
23
from werkzeug import Request, Response
1✔
24

25
from localstack import config
1✔
26
from localstack.aws.api.cloudformation import CreateChangeSetInput, Parameter
1✔
27
from localstack.aws.api.ec2 import CreateSecurityGroupRequest, CreateVpcEndpointRequest, VpcEndpoint
1✔
28
from localstack.aws.connect import ServiceLevelClientFactory
1✔
29
from localstack.services.stores import (
1✔
30
    AccountRegionBundle,
31
    BaseStore,
32
    CrossAccountAttribute,
33
    CrossRegionAttribute,
34
    LocalAttribute,
35
)
36
from localstack.testing.aws.cloudformation_utils import load_template_file, render_template
1✔
37
from localstack.testing.aws.util import get_lambda_logs, is_aws_cloud, wait_for_user
1✔
38
from localstack.testing.config import (
1✔
39
    SECONDARY_TEST_AWS_ACCOUNT_ID,
40
    SECONDARY_TEST_AWS_REGION_NAME,
41
    TEST_AWS_ACCOUNT_ID,
42
    TEST_AWS_REGION_NAME,
43
)
44
from localstack.utils import testutil
1✔
45
from localstack.utils.aws.arns import get_partition
1✔
46
from localstack.utils.aws.client import SigningHttpClient
1✔
47
from localstack.utils.aws.resources import create_dynamodb_table
1✔
48
from localstack.utils.bootstrap import is_api_enabled
1✔
49
from localstack.utils.collections import ensure_list, select_from_typed_dict
1✔
50
from localstack.utils.functions import call_safe, run_safe
1✔
51
from localstack.utils.http import safe_requests as requests
1✔
52
from localstack.utils.id_generator import ResourceIdentifier, localstack_id_manager
1✔
53
from localstack.utils.json import CustomEncoder, json_safe
1✔
54
from localstack.utils.net import wait_for_port_open
1✔
55
from localstack.utils.strings import short_uid, to_str
1✔
56
from localstack.utils.sync import ShortCircuitWaitException, poll_condition, retry, wait_until
1✔
57

58
LOG = logging.getLogger(__name__)
1✔
59

60
# URL of public HTTP echo server, used primarily for AWS parity/snapshot testing
61
PUBLIC_HTTP_ECHO_SERVER_URL = "http://httpbin.org"
1✔
62

63
WAITER_CHANGE_SET_CREATE_COMPLETE = "change_set_create_complete"
1✔
64
WAITER_STACK_CREATE_COMPLETE = "stack_create_complete"
1✔
65
WAITER_STACK_UPDATE_COMPLETE = "stack_update_complete"
1✔
66
WAITER_STACK_DELETE_COMPLETE = "stack_delete_complete"
1✔
67

68

69
if TYPE_CHECKING:
1✔
70
    from mypy_boto3_sqs import SQSClient
×
71
    from mypy_boto3_sqs.type_defs import MessageTypeDef
×
72

73

74
@pytest.fixture(scope="session")
1✔
75
def aws_client_no_retry(aws_client_factory):
1✔
76
    """
77
    This fixture can be used to obtain Boto clients with disabled retries for testing.
78
    botocore docs: https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#configuring-a-retry-mode
79

80
    Use this client when testing exceptions (i.e., with pytest.raises(...)) or expected errors (e.g., status code 500)
81
    to avoid unnecessary retries and mitigate test flakiness if the tested error condition is time-bound.
82

83
    This client is needed for the following errors, exceptions, and HTTP status codes defined by the legacy retry mode:
84
    https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#legacy-retry-mode
85
    General socket/connection errors:
86
    * ConnectionError
87
    * ConnectionClosedError
88
    * ReadTimeoutError
89
    * EndpointConnectionError
90

91
    Service-side throttling/limit errors and exceptions:
92
    * Throttling
93
    * ThrottlingException
94
    * ThrottledException
95
    * RequestThrottledException
96
    * ProvisionedThroughputExceededException
97

98
    HTTP status codes: 429, 500, 502, 503, 504, and 509
99

100
    Hence, this client is not needed for a `ResourceNotFound` error (but it doesn't harm).
101
    """
102
    no_retry_config = botocore.config.Config(retries={"max_attempts": 1})
1✔
103
    return aws_client_factory(config=no_retry_config)
1✔
104

105

106
@pytest.fixture(scope="class")
1✔
107
def aws_http_client_factory(aws_session):
1✔
108
    """
109
    Returns a factory for creating new ``SigningHttpClient`` instances using a configurable botocore request signer.
110
    The default signer is a SigV4QueryAuth. The credentials are extracted from the ``boto3_sessions`` fixture that
111
    transparently uses your global profile when TEST_TARGET=AWS_CLOUD, or test credentials when running against
112
    LocalStack.
113

114
    Example invocations
115

116
        client = aws_signing_http_client_factory("sqs")
117
        client.get("http://localhost:4566/000000000000/my-queue")
118

119
    or
120
        client = aws_signing_http_client_factory("dynamodb", signer_factory=SigV4Auth)
121
        client.post("...")
122
    """
123

124
    def factory(
1✔
125
        service: str,
126
        region: str = None,
127
        signer_factory: Callable[
128
            [botocore.credentials.Credentials, str, str], botocore.auth.BaseSigner
129
        ] = botocore.auth.SigV4QueryAuth,
130
        endpoint_url: str = None,
131
        aws_access_key_id: str = None,
132
        aws_secret_access_key: str = None,
133
    ):
134
        region = region or TEST_AWS_REGION_NAME
1✔
135

136
        if aws_access_key_id or aws_secret_access_key:
1✔
137
            credentials = botocore.credentials.Credentials(
1✔
138
                access_key=aws_access_key_id, secret_key=aws_secret_access_key
139
            )
140
        else:
141
            credentials = aws_session.get_credentials()
1✔
142

143
        creds = credentials.get_frozen_credentials()
1✔
144

145
        if not endpoint_url:
1✔
146
            if is_aws_cloud():
1✔
147
                # FIXME: this is a bit raw. we should probably re-use boto in a better way
148
                resolver: EndpointResolver = aws_session._session.get_component("endpoint_resolver")
×
149
                endpoint_url = "https://" + resolver.construct_endpoint(service, region)["hostname"]
×
150
            else:
151
                endpoint_url = config.internal_service_url()
1✔
152

153
        return SigningHttpClient(signer_factory(creds, service, region), endpoint_url=endpoint_url)
1✔
154

155
    return factory
1✔
156

157

158
@pytest.fixture(scope="class")
1✔
159
def s3_vhost_client(aws_client_factory, region_name):
1✔
160
    return aws_client_factory(
1✔
161
        config=botocore.config.Config(s3={"addressing_style": "virtual"}), region_name=region_name
162
    ).s3
163

164

165
@pytest.fixture
1✔
166
def dynamodb_wait_for_table_active(aws_client):
1✔
167
    def wait_for_table_active(table_name: str, client=None):
1✔
168
        def wait():
1✔
169
            return (client or aws_client.dynamodb).describe_table(TableName=table_name)["Table"][
1✔
170
                "TableStatus"
171
            ] == "ACTIVE"
172

173
        poll_condition(wait, timeout=30)
1✔
174

175
    return wait_for_table_active
1✔
176

177

178
@pytest.fixture
1✔
179
def dynamodb_create_table_with_parameters(dynamodb_wait_for_table_active, aws_client):
1✔
180
    tables = []
1✔
181

182
    def factory(**kwargs):
1✔
183
        if "TableName" not in kwargs:
1✔
184
            kwargs["TableName"] = f"test-table-{short_uid()}"
×
185

186
        tables.append(kwargs["TableName"])
1✔
187
        response = aws_client.dynamodb.create_table(**kwargs)
1✔
188
        dynamodb_wait_for_table_active(kwargs["TableName"])
1✔
189
        return response
1✔
190

191
    yield factory
1✔
192

193
    # cleanup
194
    for table in tables:
1✔
195
        try:
1✔
196
            # table has to be in ACTIVE state before deletion
197
            dynamodb_wait_for_table_active(table)
1✔
198
            aws_client.dynamodb.delete_table(TableName=table)
1✔
199
        except Exception as e:
1✔
200
            LOG.debug("error cleaning up table %s: %s", table, e)
1✔
201

202

203
@pytest.fixture
1✔
204
def dynamodb_create_table(dynamodb_wait_for_table_active, aws_client):
1✔
205
    # beware, this swallows exception in create_dynamodb_table utility function
206
    tables = []
1✔
207

208
    def factory(**kwargs):
1✔
209
        kwargs["client"] = aws_client.dynamodb
1✔
210
        if "table_name" not in kwargs:
1✔
211
            kwargs["table_name"] = f"test-table-{short_uid()}"
1✔
212
        if "partition_key" not in kwargs:
1✔
213
            kwargs["partition_key"] = "id"
1✔
214

215
        tables.append(kwargs["table_name"])
1✔
216

217
        return create_dynamodb_table(**kwargs)
1✔
218

219
    yield factory
1✔
220

221
    # cleanup
222
    for table in tables:
1✔
223
        try:
1✔
224
            # table has to be in ACTIVE state before deletion
225
            dynamodb_wait_for_table_active(table)
1✔
226
            aws_client.dynamodb.delete_table(TableName=table)
1✔
227
        except Exception as e:
1✔
228
            LOG.debug("error cleaning up table %s: %s", table, e)
1✔
229

230

231
@pytest.fixture
1✔
232
def s3_create_bucket(s3_empty_bucket, aws_client):
1✔
233
    buckets = []
1✔
234

235
    def factory(**kwargs) -> str:
1✔
236
        if "Bucket" not in kwargs:
1✔
237
            kwargs["Bucket"] = f"test-bucket-{short_uid()}"
1✔
238

239
        if (
1✔
240
            "CreateBucketConfiguration" not in kwargs
241
            and aws_client.s3.meta.region_name != "us-east-1"
242
        ):
243
            kwargs["CreateBucketConfiguration"] = {
×
244
                "LocationConstraint": aws_client.s3.meta.region_name
245
            }
246

247
        aws_client.s3.create_bucket(**kwargs)
1✔
248
        buckets.append(kwargs["Bucket"])
1✔
249
        return kwargs["Bucket"]
1✔
250

251
    yield factory
1✔
252

253
    # cleanup
254
    for bucket in buckets:
1✔
255
        try:
1✔
256
            s3_empty_bucket(bucket)
1✔
257
            aws_client.s3.delete_bucket(Bucket=bucket)
1✔
258
        except Exception as e:
1✔
259
            LOG.debug("error cleaning up bucket %s: %s", bucket, e)
1✔
260

261

262
@pytest.fixture
1✔
263
def s3_create_bucket_with_client(s3_empty_bucket, aws_client):
1✔
264
    buckets = []
1✔
265

266
    def factory(s3_client, **kwargs) -> str:
1✔
267
        if "Bucket" not in kwargs:
1✔
268
            kwargs["Bucket"] = f"test-bucket-{short_uid()}"
1✔
269

270
        response = s3_client.create_bucket(**kwargs)
1✔
271
        buckets.append(kwargs["Bucket"])
1✔
272
        return response
1✔
273

274
    yield factory
1✔
275

276
    # cleanup
277
    for bucket in buckets:
1✔
278
        try:
1✔
279
            s3_empty_bucket(bucket)
1✔
280
            aws_client.s3.delete_bucket(Bucket=bucket)
1✔
281
        except Exception as e:
1✔
282
            LOG.debug("error cleaning up bucket %s: %s", bucket, e)
1✔
283

284

285
@pytest.fixture
1✔
286
def s3_bucket(s3_create_bucket, aws_client) -> str:
1✔
287
    region = aws_client.s3.meta.region_name
1✔
288
    kwargs = {}
1✔
289
    if region != "us-east-1":
1✔
290
        kwargs["CreateBucketConfiguration"] = {"LocationConstraint": region}
×
291
    return s3_create_bucket(**kwargs)
1✔
292

293

294
@pytest.fixture
1✔
295
def s3_empty_bucket(aws_client):
1✔
296
    """
297
    Returns a factory that given a bucket name, deletes all objects and deletes all object versions
298
    """
299

300
    # Boto resource would make this a straightforward task, but our internal client does not support Boto resource
301
    # FIXME: this won't work when bucket has more than 1000 objects
302
    def factory(bucket_name: str):
1✔
303
        kwargs = {}
1✔
304
        try:
1✔
305
            aws_client.s3.get_object_lock_configuration(Bucket=bucket_name)
1✔
306
            kwargs["BypassGovernanceRetention"] = True
1✔
307
        except ClientError:
1✔
308
            pass
1✔
309

310
        response = aws_client.s3.list_objects_v2(Bucket=bucket_name)
1✔
311
        objects = [{"Key": obj["Key"]} for obj in response.get("Contents", [])]
1✔
312
        if objects:
1✔
313
            aws_client.s3.delete_objects(
1✔
314
                Bucket=bucket_name,
315
                Delete={"Objects": objects},
316
                **kwargs,
317
            )
318

319
        response = aws_client.s3.list_object_versions(Bucket=bucket_name)
1✔
320
        versions = response.get("Versions", [])
1✔
321
        versions.extend(response.get("DeleteMarkers", []))
1✔
322

323
        object_versions = [{"Key": obj["Key"], "VersionId": obj["VersionId"]} for obj in versions]
1✔
324
        if object_versions:
1✔
325
            aws_client.s3.delete_objects(
1✔
326
                Bucket=bucket_name,
327
                Delete={"Objects": object_versions},
328
                **kwargs,
329
            )
330

331
    yield factory
1✔
332

333

334
@pytest.fixture
1✔
335
def sqs_create_queue(aws_client):
1✔
336
    queue_urls = []
1✔
337

338
    def factory(**kwargs):
1✔
339
        if "QueueName" not in kwargs:
1✔
340
            kwargs["QueueName"] = f"test-queue-{short_uid()}"
1✔
341

342
        response = aws_client.sqs.create_queue(**kwargs)
1✔
343
        url = response["QueueUrl"]
1✔
344
        queue_urls.append(url)
1✔
345

346
        return url
1✔
347

348
    yield factory
1✔
349

350
    # cleanup
351
    for queue_url in queue_urls:
1✔
352
        try:
1✔
353
            aws_client.sqs.delete_queue(QueueUrl=queue_url)
1✔
354
        except Exception as e:
1✔
355
            LOG.debug("error cleaning up queue %s: %s", queue_url, e)
1✔
356

357

358
@pytest.fixture
1✔
359
def sqs_receive_messages_delete(aws_client):
1✔
360
    def factory(
1✔
361
        queue_url: str,
362
        expected_messages: int | None = None,
363
        wait_time: int | None = 5,
364
    ):
365
        response = aws_client.sqs.receive_message(
1✔
366
            QueueUrl=queue_url,
367
            MessageAttributeNames=["All"],
368
            VisibilityTimeout=0,
369
            WaitTimeSeconds=wait_time,
370
        )
371
        messages = []
1✔
372
        for m in response["Messages"]:
1✔
373
            message = json.loads(to_str(m["Body"]))
1✔
374
            messages.append(message)
1✔
375

376
        if expected_messages is not None:
1✔
377
            assert len(messages) == expected_messages
×
378

379
        for message in response["Messages"]:
1✔
380
            aws_client.sqs.delete_message(
1✔
381
                QueueUrl=queue_url, ReceiptHandle=message["ReceiptHandle"]
382
            )
383

384
        return messages
1✔
385

386
    return factory
1✔
387

388

389
@pytest.fixture
1✔
390
def sqs_receive_num_messages(sqs_receive_messages_delete):
1✔
391
    def factory(queue_url: str, expected_messages: int, max_iterations: int = 3):
1✔
392
        all_messages = []
1✔
393
        for _ in range(max_iterations):
1✔
394
            try:
1✔
395
                messages = sqs_receive_messages_delete(queue_url, wait_time=5)
1✔
396
            except KeyError:
×
397
                # there were no messages
398
                continue
×
399
            all_messages.extend(messages)
1✔
400

401
            if len(all_messages) >= expected_messages:
1✔
402
                return all_messages[:expected_messages]
1✔
403

404
        raise AssertionError(f"max iterations reached with {len(all_messages)} messages received")
×
405

406
    return factory
1✔
407

408

409
@pytest.fixture
1✔
410
def sqs_collect_messages(aws_client):
1✔
411
    """Collects SQS messages from a given queue_url and deletes them by default.
412
    Example usage:
413
    messages = sqs_collect_messages(
414
         my_queue_url,
415
         expected=2,
416
         timeout=10,
417
         attribute_names=["All"],
418
         message_attribute_names=["All"],
419
    )
420
    """
421

422
    def factory(
1✔
423
        queue_url: str,
424
        expected: int,
425
        timeout: int,
426
        delete: bool = True,
427
        attribute_names: list[str] = None,
428
        message_attribute_names: list[str] = None,
429
        max_number_of_messages: int = 1,
430
        wait_time_seconds: int = 5,
431
        sqs_client: "SQSClient | None" = None,
432
    ) -> list["MessageTypeDef"]:
433
        sqs_client = sqs_client or aws_client.sqs
1✔
434
        collected = []
1✔
435

436
        def _receive():
1✔
437
            response = sqs_client.receive_message(
1✔
438
                QueueUrl=queue_url,
439
                # Maximum is 20 seconds. Performs long polling.
440
                WaitTimeSeconds=wait_time_seconds,
441
                # Maximum 10 messages
442
                MaxNumberOfMessages=max_number_of_messages,
443
                AttributeNames=attribute_names or [],
444
                MessageAttributeNames=message_attribute_names or [],
445
            )
446

447
            if messages := response.get("Messages"):
1✔
448
                collected.extend(messages)
1✔
449

450
                if delete:
1✔
451
                    for m in messages:
1✔
452
                        sqs_client.delete_message(
1✔
453
                            QueueUrl=queue_url, ReceiptHandle=m["ReceiptHandle"]
454
                        )
455

456
            return len(collected) >= expected
1✔
457

458
        if not poll_condition(_receive, timeout=timeout):
1✔
459
            raise TimeoutError(
×
460
                f"gave up waiting for messages (expected={expected}, actual={len(collected)}"
461
            )
462

463
        return collected
1✔
464

465
    yield factory
1✔
466

467

468
@pytest.fixture
1✔
469
def sqs_queue(sqs_create_queue):
1✔
470
    return sqs_create_queue()
1✔
471

472

473
@pytest.fixture
1✔
474
def sqs_get_queue_arn(aws_client) -> Callable:
1✔
475
    def _get_queue_arn(queue_url: str) -> str:
1✔
476
        return aws_client.sqs.get_queue_attributes(QueueUrl=queue_url, AttributeNames=["QueueArn"])[
1✔
477
            "Attributes"
478
        ]["QueueArn"]
479

480
    return _get_queue_arn
1✔
481

482

483
@pytest.fixture
1✔
484
def sqs_queue_exists(aws_client):
1✔
485
    def _queue_exists(queue_url: str) -> bool:
1✔
486
        """
487
        Checks whether a queue with the given queue URL exists.
488
        :param queue_url: the queue URL
489
        :return: true if the queue exists, false otherwise
490
        """
491
        try:
1✔
492
            result = aws_client.sqs.get_queue_url(QueueName=queue_url.split("/")[-1])
1✔
493
            return result.get("QueueUrl") == queue_url
×
494
        except ClientError as e:
1✔
495
            if "NonExistentQueue" in e.response["Error"]["Code"]:
1✔
496
                return False
1✔
497
            raise
×
498

499
    yield _queue_exists
1✔
500

501

502
@pytest.fixture
1✔
503
def sns_create_topic(aws_client):
1✔
504
    topic_arns = []
1✔
505

506
    def _create_topic(**kwargs):
1✔
507
        if "Name" not in kwargs:
1✔
508
            kwargs["Name"] = f"test-topic-{short_uid()}"
1✔
509
        response = aws_client.sns.create_topic(**kwargs)
1✔
510
        topic_arns.append(response["TopicArn"])
1✔
511
        return response
1✔
512

513
    yield _create_topic
1✔
514

515
    for topic_arn in topic_arns:
1✔
516
        try:
1✔
517
            aws_client.sns.delete_topic(TopicArn=topic_arn)
1✔
518
        except Exception as e:
×
519
            LOG.debug("error cleaning up topic %s: %s", topic_arn, e)
×
520

521

522
@pytest.fixture
1✔
523
def sns_wait_for_topic_delete(aws_client):
1✔
524
    def wait_for_topic_delete(topic_arn: str) -> None:
1✔
525
        def wait():
1✔
526
            try:
1✔
527
                aws_client.sns.get_topic_attributes(TopicArn=topic_arn)
1✔
528
                return False
×
529
            except Exception as e:
1✔
530
                if "NotFound" in e.response["Error"]["Code"]:
1✔
531
                    return True
1✔
532

533
                raise
×
534

535
        poll_condition(wait, timeout=30)
1✔
536

537
    return wait_for_topic_delete
1✔
538

539

540
@pytest.fixture
1✔
541
def sns_subscription(aws_client):
1✔
542
    sub_arns = []
1✔
543

544
    def _create_sub(**kwargs):
1✔
545
        if kwargs.get("ReturnSubscriptionArn") is None:
1✔
546
            kwargs["ReturnSubscriptionArn"] = True
1✔
547

548
        # requires 'TopicArn', 'Protocol', and 'Endpoint'
549
        response = aws_client.sns.subscribe(**kwargs)
1✔
550
        sub_arn = response["SubscriptionArn"]
1✔
551
        sub_arns.append(sub_arn)
1✔
552
        return response
1✔
553

554
    yield _create_sub
1✔
555

556
    for sub_arn in sub_arns:
1✔
557
        try:
1✔
558
            aws_client.sns.unsubscribe(SubscriptionArn=sub_arn)
1✔
559
        except Exception as e:
1✔
560
            LOG.debug("error cleaning up subscription %s: %s", sub_arn, e)
1✔
561

562

563
@pytest.fixture
1✔
564
def sns_topic(sns_create_topic, aws_client):
1✔
565
    topic_arn = sns_create_topic()["TopicArn"]
1✔
566
    return aws_client.sns.get_topic_attributes(TopicArn=topic_arn)
1✔
567

568

569
@pytest.fixture
1✔
570
def sns_allow_topic_sqs_queue(aws_client):
1✔
571
    def _allow_sns_topic(sqs_queue_url, sqs_queue_arn, sns_topic_arn) -> None:
1✔
572
        # allow topic to write to sqs queue
573
        aws_client.sqs.set_queue_attributes(
1✔
574
            QueueUrl=sqs_queue_url,
575
            Attributes={
576
                "Policy": json.dumps(
577
                    {
578
                        "Statement": [
579
                            {
580
                                "Effect": "Allow",
581
                                "Principal": {"Service": "sns.amazonaws.com"},
582
                                "Action": "sqs:SendMessage",
583
                                "Resource": sqs_queue_arn,
584
                                "Condition": {"ArnEquals": {"aws:SourceArn": sns_topic_arn}},
585
                            }
586
                        ]
587
                    }
588
                )
589
            },
590
        )
591

592
    return _allow_sns_topic
1✔
593

594

595
@pytest.fixture
1✔
596
def sns_create_sqs_subscription(sns_allow_topic_sqs_queue, sqs_get_queue_arn, aws_client):
1✔
597
    subscriptions = []
1✔
598

599
    def _factory(topic_arn: str, queue_url: str, **kwargs) -> dict[str, str]:
1✔
600
        queue_arn = sqs_get_queue_arn(queue_url)
1✔
601

602
        # connect sns topic to sqs
603
        subscription = aws_client.sns.subscribe(
1✔
604
            TopicArn=topic_arn, Protocol="sqs", Endpoint=queue_arn, **kwargs
605
        )
606
        subscription_arn = subscription["SubscriptionArn"]
1✔
607

608
        # allow topic to write to sqs queue
609
        sns_allow_topic_sqs_queue(
1✔
610
            sqs_queue_url=queue_url, sqs_queue_arn=queue_arn, sns_topic_arn=topic_arn
611
        )
612

613
        subscriptions.append(subscription_arn)
1✔
614
        return aws_client.sns.get_subscription_attributes(SubscriptionArn=subscription_arn)[
1✔
615
            "Attributes"
616
        ]
617

618
    yield _factory
1✔
619

620
    for arn in subscriptions:
1✔
621
        try:
1✔
622
            aws_client.sns.unsubscribe(SubscriptionArn=arn)
1✔
623
        except Exception as e:
×
624
            LOG.error("error cleaning up subscription %s: %s", arn, e)
×
625

626

627
@pytest.fixture
1✔
628
def sns_create_http_endpoint(sns_create_topic, sns_subscription, aws_client):
1✔
629
    # This fixture can be used with manual setup to expose the HTTPServer fixture to AWS. One example is to use a
630
    # a service like localhost.run, and set up a specific port to start the `HTTPServer(port=40000)` for example,
631
    # and tunnel `localhost:40000` to a specific domain that you can manually return from this fixture.
632
    http_servers = []
1✔
633

634
    def _create_http_endpoint(
1✔
635
        raw_message_delivery: bool = False,
636
    ) -> tuple[str, str, str, HTTPServer]:
637
        server = HTTPServer()
1✔
638
        server.start()
1✔
639
        http_servers.append(server)
1✔
640
        server.expect_request("/sns-endpoint").respond_with_data(status=200)
1✔
641
        endpoint_url = server.url_for("/sns-endpoint")
1✔
642
        wait_for_port_open(endpoint_url)
1✔
643

644
        topic_arn = sns_create_topic()["TopicArn"]
1✔
645
        subscription = sns_subscription(TopicArn=topic_arn, Protocol="http", Endpoint=endpoint_url)
1✔
646
        subscription_arn = subscription["SubscriptionArn"]
1✔
647
        delivery_policy = {
1✔
648
            "healthyRetryPolicy": {
649
                "minDelayTarget": 1,
650
                "maxDelayTarget": 1,
651
                "numRetries": 0,
652
                "numNoDelayRetries": 0,
653
                "numMinDelayRetries": 0,
654
                "numMaxDelayRetries": 0,
655
                "backoffFunction": "linear",
656
            },
657
            "sicklyRetryPolicy": None,
658
            "throttlePolicy": {"maxReceivesPerSecond": 1000},
659
            "guaranteed": False,
660
        }
661
        aws_client.sns.set_subscription_attributes(
1✔
662
            SubscriptionArn=subscription_arn,
663
            AttributeName="DeliveryPolicy",
664
            AttributeValue=json.dumps(delivery_policy),
665
        )
666

667
        if raw_message_delivery:
1✔
668
            aws_client.sns.set_subscription_attributes(
1✔
669
                SubscriptionArn=subscription_arn,
670
                AttributeName="RawMessageDelivery",
671
                AttributeValue="true",
672
            )
673

674
        return topic_arn, subscription_arn, endpoint_url, server
1✔
675

676
    yield _create_http_endpoint
1✔
677

678
    for http_server in http_servers:
1✔
679
        if http_server.is_running():
1✔
680
            http_server.stop()
1✔
681

682

683
@pytest.fixture
1✔
684
def route53_hosted_zone(aws_client):
1✔
685
    hosted_zones = []
1✔
686

687
    def factory(**kwargs):
1✔
688
        if "Name" not in kwargs:
1✔
689
            kwargs["Name"] = f"www.{short_uid()}.com."
1✔
690
        if "CallerReference" not in kwargs:
1✔
691
            kwargs["CallerReference"] = f"caller-ref-{short_uid()}"
1✔
692
        response = aws_client.route53.create_hosted_zone(
1✔
693
            Name=kwargs["Name"], CallerReference=kwargs["CallerReference"]
694
        )
695
        hosted_zones.append(response["HostedZone"]["Id"])
1✔
696
        return response
1✔
697

698
    yield factory
1✔
699

700
    for zone in hosted_zones:
1✔
701
        try:
1✔
702
            aws_client.route53.delete_hosted_zone(Id=zone)
1✔
703
        except Exception as e:
1✔
704
            LOG.debug("error cleaning up route53 HostedZone %s: %s", zone, e)
1✔
705

706

707
@pytest.fixture
1✔
708
def transcribe_create_job(s3_bucket, aws_client):
1✔
709
    job_names = []
1✔
710

711
    def _create_job(audio_file: str, params: dict[str, Any] | None = None) -> str:
1✔
712
        s3_key = "test-clip.wav"
1✔
713

714
        if not params:
1✔
715
            params = {}
1✔
716

717
        if "TranscriptionJobName" not in params:
1✔
718
            params["TranscriptionJobName"] = f"test-transcribe-{short_uid()}"
1✔
719

720
        if "LanguageCode" not in params:
1✔
721
            params["LanguageCode"] = "en-GB"
1✔
722

723
        if "Media" not in params:
1✔
724
            params["Media"] = {"MediaFileUri": f"s3://{s3_bucket}/{s3_key}"}
1✔
725

726
        # upload test wav to a s3 bucket
727
        with open(audio_file, "rb") as f:
1✔
728
            aws_client.s3.upload_fileobj(f, s3_bucket, s3_key)
1✔
729

730
        response = aws_client.transcribe.start_transcription_job(**params)
1✔
731

732
        job_name = response["TranscriptionJob"]["TranscriptionJobName"]
1✔
733
        job_names.append(job_name)
1✔
734

735
        return job_name
1✔
736

737
    yield _create_job
1✔
738

739
    for job_name in job_names:
1✔
740
        with contextlib.suppress(ClientError):
1✔
741
            aws_client.transcribe.delete_transcription_job(TranscriptionJobName=job_name)
1✔
742

743

744
@pytest.fixture
1✔
745
def kinesis_create_stream(aws_client):
1✔
746
    stream_names = []
1✔
747

748
    def _create_stream(**kwargs):
1✔
749
        if "StreamName" not in kwargs:
1✔
750
            kwargs["StreamName"] = f"test-stream-{short_uid()}"
1✔
751
        aws_client.kinesis.create_stream(**kwargs)
1✔
752
        stream_names.append(kwargs["StreamName"])
1✔
753
        return kwargs["StreamName"]
1✔
754

755
    yield _create_stream
1✔
756

757
    for stream_name in stream_names:
1✔
758
        try:
1✔
759
            aws_client.kinesis.delete_stream(StreamName=stream_name, EnforceConsumerDeletion=True)
1✔
760
        except Exception as e:
1✔
761
            LOG.debug("error cleaning up kinesis stream %s: %s", stream_name, e)
1✔
762

763

764
@pytest.fixture
1✔
765
def wait_for_stream_ready(aws_client):
1✔
766
    def _wait_for_stream_ready(stream_name: str):
1✔
767
        def is_stream_ready():
1✔
768
            describe_stream_response = aws_client.kinesis.describe_stream(StreamName=stream_name)
1✔
769
            return describe_stream_response["StreamDescription"]["StreamStatus"] in [
1✔
770
                "ACTIVE",
771
                "UPDATING",
772
            ]
773

774
        return poll_condition(is_stream_ready)
1✔
775

776
    return _wait_for_stream_ready
1✔
777

778

779
@pytest.fixture
1✔
780
def wait_for_delivery_stream_ready(aws_client):
1✔
781
    def _wait_for_stream_ready(delivery_stream_name: str):
1✔
782
        def is_stream_ready():
1✔
783
            describe_stream_response = aws_client.firehose.describe_delivery_stream(
1✔
784
                DeliveryStreamName=delivery_stream_name
785
            )
786
            return (
1✔
787
                describe_stream_response["DeliveryStreamDescription"]["DeliveryStreamStatus"]
788
                == "ACTIVE"
789
            )
790

791
        poll_condition(is_stream_ready)
1✔
792

793
    return _wait_for_stream_ready
1✔
794

795

796
@pytest.fixture
1✔
797
def wait_for_dynamodb_stream_ready(aws_client):
1✔
798
    def _wait_for_stream_ready(stream_arn: str, client=None):
1✔
799
        def is_stream_ready():
1✔
800
            ddb_client = client or aws_client.dynamodbstreams
1✔
801
            describe_stream_response = ddb_client.describe_stream(StreamArn=stream_arn)
1✔
802
            return describe_stream_response["StreamDescription"]["StreamStatus"] == "ENABLED"
1✔
803

804
        return poll_condition(is_stream_ready)
1✔
805

806
    return _wait_for_stream_ready
1✔
807

808

809
@pytest.fixture()
1✔
810
def kms_create_key(aws_client_factory):
1✔
811
    key_ids = []
1✔
812

813
    def _create_key(region_name: str = None, **kwargs):
1✔
814
        if "Description" not in kwargs:
1✔
815
            kwargs["Description"] = f"test description - {short_uid()}"
1✔
816
        key_metadata = aws_client_factory(region_name=region_name).kms.create_key(**kwargs)[
1✔
817
            "KeyMetadata"
818
        ]
819
        key_ids.append((region_name, key_metadata["KeyId"]))
1✔
820
        return key_metadata
1✔
821

822
    yield _create_key
1✔
823

824
    for region_name, key_id in key_ids:
1✔
825
        try:
1✔
826
            # shortest amount of time you can schedule the deletion
827
            aws_client_factory(region_name=region_name).kms.schedule_key_deletion(
1✔
828
                KeyId=key_id, PendingWindowInDays=7
829
            )
830
        except Exception as e:
1✔
831
            exception_message = str(e)
1✔
832
            # Some tests schedule their keys for deletion themselves.
833
            if (
1✔
834
                "KMSInvalidStateException" not in exception_message
835
                or "is pending deletion" not in exception_message
836
            ):
837
                LOG.debug("error cleaning up KMS key %s: %s", key_id, e)
×
838

839

840
@pytest.fixture()
1✔
841
def kms_replicate_key(aws_client_factory):
1✔
842
    key_ids = []
1✔
843

844
    def _replicate_key(region_from=None, **kwargs):
1✔
845
        region_to = kwargs.get("ReplicaRegion")
1✔
846
        key_ids.append((region_to, kwargs.get("KeyId")))
1✔
847
        return aws_client_factory(region_name=region_from).kms.replicate_key(**kwargs)
1✔
848

849
    yield _replicate_key
1✔
850

851
    for region_to, key_id in key_ids:
1✔
852
        try:
1✔
853
            # shortest amount of time you can schedule the deletion
854
            aws_client_factory(region_name=region_to).kms.schedule_key_deletion(
1✔
855
                KeyId=key_id, PendingWindowInDays=7
856
            )
857
        except Exception as e:
×
858
            LOG.debug("error cleaning up KMS key %s: %s", key_id, e)
×
859

860

861
# kms_create_key fixture is used here not just to be able to create aliases without a key specified,
862
# but also to make sure that kms_create_key gets executed before and teared down after kms_create_alias -
863
# to make sure that we clean up aliases before keys get cleaned up.
864
@pytest.fixture()
1✔
865
def kms_create_alias(kms_create_key, aws_client):
1✔
866
    aliases = []
1✔
867

868
    def _create_alias(**kwargs):
1✔
869
        if "AliasName" not in kwargs:
1✔
870
            kwargs["AliasName"] = f"alias/{short_uid()}"
1✔
871
        if "TargetKeyId" not in kwargs:
1✔
872
            kwargs["TargetKeyId"] = kms_create_key()["KeyId"]
1✔
873

874
        aws_client.kms.create_alias(**kwargs)
1✔
875
        aliases.append(kwargs["AliasName"])
1✔
876
        return kwargs["AliasName"]
1✔
877

878
    yield _create_alias
1✔
879

880
    for alias in aliases:
1✔
881
        try:
1✔
882
            aws_client.kms.delete_alias(AliasName=alias)
1✔
883
        except Exception as e:
1✔
884
            LOG.debug("error cleaning up KMS alias %s: %s", alias, e)
1✔
885

886

887
@pytest.fixture()
1✔
888
def kms_create_grant(kms_create_key, aws_client):
1✔
889
    grants = []
1✔
890

891
    def _create_grant(**kwargs):
1✔
892
        # Just a random ARN, since KMS in LocalStack currently doesn't validate GranteePrincipal,
893
        # but some GranteePrincipal is required to create a grant.
894
        GRANTEE_PRINCIPAL_ARN = (
1✔
895
            "arn:aws:kms:eu-central-1:123456789876:key/198a5a78-52c3-489f-ac70-b06a4d11027a"
896
        )
897

898
        if "Operations" not in kwargs:
1✔
899
            kwargs["Operations"] = ["Decrypt", "Encrypt"]
1✔
900
        if "GranteePrincipal" not in kwargs:
1✔
901
            kwargs["GranteePrincipal"] = GRANTEE_PRINCIPAL_ARN
1✔
902
        if "KeyId" not in kwargs:
1✔
903
            kwargs["KeyId"] = kms_create_key()["KeyId"]
×
904

905
        grant_id = aws_client.kms.create_grant(**kwargs)["GrantId"]
1✔
906
        grants.append((grant_id, kwargs["KeyId"]))
1✔
907
        return grant_id, kwargs["KeyId"]
1✔
908

909
    yield _create_grant
1✔
910

911
    for grant_id, key_id in grants:
1✔
912
        try:
1✔
913
            aws_client.kms.retire_grant(GrantId=grant_id, KeyId=key_id)
1✔
914
        except Exception as e:
×
915
            LOG.debug("error cleaning up KMS grant %s: %s", grant_id, e)
×
916

917

918
@pytest.fixture
1✔
919
def kms_key(kms_create_key):
1✔
920
    return kms_create_key()
1✔
921

922

923
@pytest.fixture
1✔
924
def kms_grant_and_key(kms_key, aws_client):
1✔
925
    user_arn = aws_client.sts.get_caller_identity()["Arn"]
1✔
926

927
    return [
1✔
928
        aws_client.kms.create_grant(
929
            KeyId=kms_key["KeyId"],
930
            GranteePrincipal=user_arn,
931
            Operations=["Decrypt", "Encrypt"],
932
        ),
933
        kms_key,
934
    ]
935

936

937
@pytest.fixture
1✔
938
def opensearch_wait_for_cluster(aws_client):
1✔
939
    def _wait_for_cluster(domain_name: str):
1✔
940
        def finished_processing():
1✔
941
            status = aws_client.opensearch.describe_domain(DomainName=domain_name)["DomainStatus"]
1✔
942
            return status["DomainProcessingStatus"] == "Active" and "Endpoint" in status
1✔
943

944
        assert poll_condition(
1✔
945
            finished_processing, timeout=25 * 60, **({"interval": 10} if is_aws_cloud() else {})
946
        ), f"could not start domain: {domain_name}"
947

948
    return _wait_for_cluster
1✔
949

950

951
@pytest.fixture
1✔
952
def opensearch_create_domain(opensearch_wait_for_cluster, aws_client):
1✔
953
    domains = []
1✔
954

955
    def factory(**kwargs) -> str:
1✔
956
        if "DomainName" not in kwargs:
1✔
957
            kwargs["DomainName"] = f"test-domain-{short_uid()}"
1✔
958

959
        aws_client.opensearch.create_domain(**kwargs)
1✔
960

961
        opensearch_wait_for_cluster(domain_name=kwargs["DomainName"])
1✔
962

963
        domains.append(kwargs["DomainName"])
1✔
964
        return kwargs["DomainName"]
1✔
965

966
    yield factory
1✔
967

968
    # cleanup
969
    for domain in domains:
1✔
970
        try:
1✔
971
            aws_client.opensearch.delete_domain(DomainName=domain)
1✔
972
        except Exception as e:
×
973
            LOG.debug("error cleaning up domain %s: %s", domain, e)
×
974

975

976
@pytest.fixture
1✔
977
def opensearch_domain(opensearch_create_domain) -> str:
1✔
978
    return opensearch_create_domain()
1✔
979

980

981
@pytest.fixture
1✔
982
def opensearch_endpoint(opensearch_domain, aws_client) -> str:
1✔
983
    status = aws_client.opensearch.describe_domain(DomainName=opensearch_domain)["DomainStatus"]
1✔
984
    assert "Endpoint" in status
1✔
985
    return f"https://{status['Endpoint']}"
1✔
986

987

988
@pytest.fixture
1✔
989
def opensearch_document_path(opensearch_endpoint, aws_client):
1✔
990
    document = {
1✔
991
        "first_name": "Boba",
992
        "last_name": "Fett",
993
        "age": 41,
994
        "about": "I'm just a simple man, trying to make my way in the universe.",
995
        "interests": ["mandalorian armor", "tusken culture"],
996
    }
997
    document_path = f"{opensearch_endpoint}/bountyhunters/_doc/1"
1✔
998
    response = requests.put(
1✔
999
        document_path,
1000
        data=json.dumps(document),
1001
        headers={"content-type": "application/json", "Accept-encoding": "identity"},
1002
    )
1003
    assert response.status_code == 201, f"could not create document at: {document_path}"
1✔
1004
    return document_path
1✔
1005

1006

1007
# Cleanup fixtures
1008
@pytest.fixture
1✔
1009
def cleanup_stacks(aws_client):
1✔
1010
    def _cleanup_stacks(stacks: list[str]) -> None:
1✔
1011
        stacks = ensure_list(stacks)
1✔
1012
        for stack in stacks:
1✔
1013
            try:
1✔
1014
                aws_client.cloudformation.delete_stack(StackName=stack)
1✔
1015
                aws_client.cloudformation.get_waiter("stack_delete_complete").wait(StackName=stack)
1✔
1016
            except Exception:
×
1017
                LOG.debug("Failed to cleanup stack '%s'", stack)
×
1018

1019
    return _cleanup_stacks
1✔
1020

1021

1022
@pytest.fixture
1✔
1023
def cleanup_changesets(aws_client):
1✔
1024
    def _cleanup_changesets(changesets: list[str]) -> None:
1✔
1025
        changesets = ensure_list(changesets)
1✔
1026
        for cs in changesets:
1✔
1027
            try:
1✔
1028
                aws_client.cloudformation.delete_change_set(ChangeSetName=cs)
1✔
1029
            except Exception:
1✔
1030
                LOG.debug("Failed to cleanup changeset '%s'", cs)
1✔
1031

1032
    return _cleanup_changesets
1✔
1033

1034

1035
# Helpers for Cfn
1036

1037

1038
# TODO: exports(!)
1039
@dataclasses.dataclass(frozen=True)
1✔
1040
class DeployResult:
1✔
1041
    change_set_id: str
1✔
1042
    stack_id: str
1✔
1043
    stack_name: str
1✔
1044
    change_set_name: str
1✔
1045
    outputs: dict[str, str]
1✔
1046

1047
    destroy: Callable[[], None]
1✔
1048

1049

1050
class StackDeployError(Exception):
1✔
1051
    def __init__(self, describe_res: dict, events: list[dict]):
1✔
1052
        self.describe_result = describe_res
1✔
1053
        self.events = events
1✔
1054

1055
        encoded_describe_output = json.dumps(self.describe_result, cls=CustomEncoder)
1✔
1056
        if config.CFN_VERBOSE_ERRORS:
1✔
1057
            msg = f"Describe output:\n{encoded_describe_output}\nEvents:\n{self.format_events(events)}"
×
1058
        else:
1059
            msg = f"Describe output:\n{encoded_describe_output}\nFailing resources:\n{self.format_events(events)}"
1✔
1060

1061
        super().__init__(msg)
1✔
1062

1063
    def format_events(self, events: list[dict]) -> str:
1✔
1064
        formatted_events = []
1✔
1065

1066
        chronological_events = sorted(events, key=lambda event: event["Timestamp"])
1✔
1067
        for event in chronological_events:
1✔
1068
            if event["ResourceStatus"].endswith("FAILED") or config.CFN_VERBOSE_ERRORS:
1✔
1069
                formatted_events.append(self.format_event(event))
1✔
1070

1071
        return "\n".join(formatted_events)
1✔
1072

1073
    @staticmethod
1✔
1074
    def format_event(event: dict) -> str:
1✔
1075
        if reason := event.get("ResourceStatusReason"):
1✔
1076
            reason = reason.replace("\n", "; ")
1✔
1077
            return f"- {event['LogicalResourceId']} ({event['ResourceType']}) -> {event['ResourceStatus']} ({reason})"
1✔
1078
        else:
1079
            return f"- {event['LogicalResourceId']} ({event['ResourceType']}) -> {event['ResourceStatus']}"
1✔
1080

1081

1082
@pytest.fixture
1✔
1083
def deploy_cfn_template(
1✔
1084
    aws_client: ServiceLevelClientFactory,
1085
):
1086
    state: list[tuple[str, Callable]] = []
1✔
1087

1088
    def _deploy(
1✔
1089
        *,
1090
        is_update: bool | None = False,
1091
        stack_name: str | None = None,
1092
        change_set_name: str | None = None,
1093
        template: str | None = None,
1094
        template_path: str | os.PathLike | None = None,
1095
        template_mapping: dict[str, Any] | None = None,
1096
        parameters: dict[str, str] | None = None,
1097
        role_arn: str | None = None,
1098
        max_wait: int | None = None,
1099
        delay_between_polls: int | None = 2,
1100
        custom_aws_client: ServiceLevelClientFactory | None = None,
1101
        raw_parameters: list[Parameter] | None = None,
1102
    ) -> DeployResult:
1103
        if is_update:
1✔
1104
            assert stack_name
1✔
1105
        stack_name = stack_name or f"stack-{short_uid()}"
1✔
1106
        change_set_name = change_set_name or f"change-set-{short_uid()}"
1✔
1107

1108
        if max_wait is None:
1✔
1109
            max_wait = 1800 if is_aws_cloud() else 180
1✔
1110

1111
        if template_path is not None:
1✔
1112
            template = load_template_file(template_path)
1✔
1113
            if template is None:
1✔
1114
                raise RuntimeError(f"Could not find file {os.path.realpath(template_path)}")
×
1115
        template_rendered = render_template(template, **(template_mapping or {}))
1✔
1116

1117
        kwargs = CreateChangeSetInput(
1✔
1118
            StackName=stack_name,
1119
            ChangeSetName=change_set_name,
1120
            TemplateBody=template_rendered,
1121
            Capabilities=["CAPABILITY_AUTO_EXPAND", "CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
1122
            ChangeSetType=("UPDATE" if is_update else "CREATE"),
1123
        )
1124
        kwargs["Parameters"] = []
1✔
1125
        if parameters:
1✔
1126
            kwargs["Parameters"] = [
1✔
1127
                Parameter(ParameterKey=k, ParameterValue=v) for (k, v) in parameters.items()
1128
            ]
1129
        elif raw_parameters:
1✔
1130
            kwargs["Parameters"] = raw_parameters
×
1131

1132
        if role_arn is not None:
1✔
1133
            kwargs["RoleARN"] = role_arn
×
1134

1135
        cfn_aws_client = custom_aws_client if custom_aws_client is not None else aws_client
1✔
1136

1137
        response = cfn_aws_client.cloudformation.create_change_set(**kwargs)
1✔
1138

1139
        change_set_id = response["Id"]
1✔
1140
        stack_id = response["StackId"]
1✔
1141

1142
        try:
1✔
1143
            cfn_aws_client.cloudformation.get_waiter(WAITER_CHANGE_SET_CREATE_COMPLETE).wait(
1✔
1144
                ChangeSetName=change_set_id
1145
            )
1146
        except botocore.exceptions.WaiterError as e:
×
1147
            change_set = cfn_aws_client.cloudformation.describe_change_set(
×
1148
                ChangeSetName=change_set_id
1149
            )
1150
            raise Exception(f"{change_set['Status']}: {change_set.get('StatusReason')}") from e
×
1151

1152
        cfn_aws_client.cloudformation.execute_change_set(ChangeSetName=change_set_id)
1✔
1153
        stack_waiter = cfn_aws_client.cloudformation.get_waiter(
1✔
1154
            WAITER_STACK_UPDATE_COMPLETE if is_update else WAITER_STACK_CREATE_COMPLETE
1155
        )
1156

1157
        try:
1✔
1158
            stack_waiter.wait(
1✔
1159
                StackName=stack_id,
1160
                WaiterConfig={
1161
                    "Delay": delay_between_polls,
1162
                    "MaxAttempts": max_wait / delay_between_polls,
1163
                },
1164
            )
1165
        except botocore.exceptions.WaiterError as e:
1✔
1166
            raise StackDeployError(
1✔
1167
                cfn_aws_client.cloudformation.describe_stacks(StackName=stack_id)["Stacks"][0],
1168
                cfn_aws_client.cloudformation.describe_stack_events(StackName=stack_id)[
1169
                    "StackEvents"
1170
                ],
1171
            ) from e
1172

1173
        describe_stack_res = cfn_aws_client.cloudformation.describe_stacks(StackName=stack_id)[
1✔
1174
            "Stacks"
1175
        ][0]
1176
        outputs = describe_stack_res.get("Outputs", [])
1✔
1177

1178
        mapped_outputs = {o["OutputKey"]: o.get("OutputValue") for o in outputs}
1✔
1179

1180
        def _destroy_stack():
1✔
1181
            cfn_aws_client.cloudformation.delete_stack(StackName=stack_id)
1✔
1182
            cfn_aws_client.cloudformation.get_waiter(WAITER_STACK_DELETE_COMPLETE).wait(
1✔
1183
                StackName=stack_id,
1184
                WaiterConfig={
1185
                    "Delay": delay_between_polls,
1186
                    "MaxAttempts": max_wait / delay_between_polls,
1187
                },
1188
            )
1189

1190
        state.append((stack_id, _destroy_stack))
1✔
1191

1192
        return DeployResult(
1✔
1193
            change_set_id, stack_id, stack_name, change_set_name, mapped_outputs, _destroy_stack
1194
        )
1195

1196
    yield _deploy
1✔
1197

1198
    # delete the stacks in the reverse order they were created in case of inter-stack dependencies
1199
    for stack_id, teardown in state[::-1]:
1✔
1200
        try:
1✔
1201
            teardown()
1✔
1202
        except Exception as e:
1✔
1203
            LOG.debug("Failed cleaning up stack stack_id=%s: %s", stack_id, e)
1✔
1204

1205

1206
@pytest.fixture
1✔
1207
def is_change_set_created_and_available(aws_client):
1✔
1208
    def _is_change_set_created_and_available(change_set_id: str):
1✔
1209
        def _inner():
1✔
1210
            change_set = aws_client.cloudformation.describe_change_set(ChangeSetName=change_set_id)
1✔
1211
            return (
1✔
1212
                # TODO: CREATE_FAILED should also not lead to further retries
1213
                change_set.get("Status") == "CREATE_COMPLETE"
1214
                and change_set.get("ExecutionStatus") == "AVAILABLE"
1215
            )
1216

1217
        return _inner
1✔
1218

1219
    return _is_change_set_created_and_available
1✔
1220

1221

1222
@pytest.fixture
1✔
1223
def is_change_set_failed_and_unavailable(aws_client):
1✔
1224
    def _is_change_set_created_and_available(change_set_id: str):
×
1225
        def _inner():
×
1226
            change_set = aws_client.cloudformation.describe_change_set(ChangeSetName=change_set_id)
×
1227
            return (
×
1228
                # TODO: CREATE_FAILED should also not lead to further retries
1229
                change_set.get("Status") == "FAILED"
1230
                and change_set.get("ExecutionStatus") == "UNAVAILABLE"
1231
            )
1232

1233
        return _inner
×
1234

1235
    return _is_change_set_created_and_available
×
1236

1237

1238
@pytest.fixture
1✔
1239
def is_stack_created(aws_client):
1✔
1240
    return _has_stack_status(aws_client.cloudformation, ["CREATE_COMPLETE", "CREATE_FAILED"])
1✔
1241

1242

1243
@pytest.fixture
1✔
1244
def is_stack_updated(aws_client):
1✔
1245
    return _has_stack_status(aws_client.cloudformation, ["UPDATE_COMPLETE", "UPDATE_FAILED"])
×
1246

1247

1248
@pytest.fixture
1✔
1249
def is_stack_deleted(aws_client):
1✔
1250
    return _has_stack_status(aws_client.cloudformation, ["DELETE_COMPLETE"])
×
1251

1252

1253
def _has_stack_status(cfn_client, statuses: list[str]):
1✔
1254
    def _has_status(stack_id: str):
1✔
1255
        def _inner():
1✔
1256
            resp = cfn_client.describe_stacks(StackName=stack_id)
1✔
1257
            s = resp["Stacks"][0]  # since the lookup  uses the id we can only get a single response
1✔
1258
            return s.get("StackStatus") in statuses
1✔
1259

1260
        return _inner
1✔
1261

1262
    return _has_status
1✔
1263

1264

1265
@pytest.fixture
1✔
1266
def is_change_set_finished(aws_client):
1✔
1267
    def _is_change_set_finished(change_set_id: str, stack_name: str | None = None):
1✔
1268
        def _inner():
×
1269
            kwargs = {"ChangeSetName": change_set_id}
×
1270
            if stack_name:
×
1271
                kwargs["StackName"] = stack_name
×
1272

1273
            check_set = aws_client.cloudformation.describe_change_set(**kwargs)
×
1274

1275
            if check_set.get("ExecutionStatus") == "EXECUTE_FAILED":
×
1276
                LOG.warning("Change set failed")
×
1277
                raise ShortCircuitWaitException()
×
1278

1279
            return check_set.get("ExecutionStatus") == "EXECUTE_COMPLETE"
×
1280

1281
        return _inner
×
1282

1283
    return _is_change_set_finished
1✔
1284

1285

1286
@pytest.fixture
1✔
1287
def wait_until_lambda_ready(aws_client):
1✔
1288
    def _wait_until_ready(function_name: str, qualifier: str = None, client=None):
1✔
1289
        client = client or aws_client.lambda_
1✔
1290

1291
        def _is_not_pending():
1✔
1292
            kwargs = {}
1✔
1293
            if qualifier:
1✔
1294
                kwargs["Qualifier"] = qualifier
×
1295
            try:
1✔
1296
                result = (
1✔
1297
                    client.get_function(FunctionName=function_name)["Configuration"]["State"]
1298
                    != "Pending"
1299
                )
1300
                LOG.debug("lambda state result: result=%s", result)
1✔
1301
                return result
1✔
1302
            except Exception as e:
×
1303
                LOG.error(e)
×
1304
                raise
×
1305

1306
        wait_until(_is_not_pending)
1✔
1307

1308
    return _wait_until_ready
1✔
1309

1310

1311
role_assume_policy = """
1✔
1312
{
1313
  "Version": "2012-10-17",
1314
  "Statement": [
1315
    {
1316
      "Effect": "Allow",
1317
      "Principal": {
1318
        "Service": "lambda.amazonaws.com"
1319
      },
1320
      "Action": "sts:AssumeRole"
1321
    }
1322
  ]
1323
}
1324
""".strip()
1325

1326
role_policy = """
1✔
1327
{
1328
    "Version": "2012-10-17",
1329
    "Statement": [
1330
        {
1331
            "Effect": "Allow",
1332
            "Action": [
1333
                "logs:CreateLogGroup",
1334
                "logs:CreateLogStream",
1335
                "logs:PutLogEvents"
1336
            ],
1337
            "Resource": [
1338
                "*"
1339
            ]
1340
        }
1341
    ]
1342
}
1343
""".strip()
1344

1345

1346
@pytest.fixture
1✔
1347
def create_lambda_function_aws(aws_client):
1✔
1348
    lambda_arns = []
1✔
1349

1350
    def _create_lambda_function(**kwargs):
1✔
1351
        def _create_function():
1✔
1352
            resp = aws_client.lambda_.create_function(**kwargs)
1✔
1353
            lambda_arns.append(resp["FunctionArn"])
1✔
1354

1355
            def _is_not_pending():
1✔
1356
                try:
1✔
1357
                    result = (
1✔
1358
                        aws_client.lambda_.get_function(FunctionName=resp["FunctionName"])[
1359
                            "Configuration"
1360
                        ]["State"]
1361
                        != "Pending"
1362
                    )
1363
                    return result
1✔
1364
                except Exception as e:
×
1365
                    LOG.error(e)
×
1366
                    raise
×
1367

1368
            wait_until(_is_not_pending)
1✔
1369
            return resp
1✔
1370

1371
        # @AWS, takes about 10s until the role/policy is "active", until then it will fail
1372
        # localstack should normally not require the retries and will just continue here
1373
        return retry(_create_function, retries=3, sleep=4)
1✔
1374

1375
    yield _create_lambda_function
1✔
1376

1377
    for arn in lambda_arns:
1✔
1378
        try:
1✔
1379
            aws_client.lambda_.delete_function(FunctionName=arn)
1✔
1380
        except Exception:
×
1381
            LOG.debug("Unable to delete function arn=%s in cleanup", arn)
×
1382

1383

1384
@pytest.fixture
1✔
1385
def create_lambda_function(aws_client, wait_until_lambda_ready, lambda_su_role):
1✔
1386
    lambda_arns_and_clients = []
1✔
1387
    log_groups = []
1✔
1388
    lambda_client = aws_client.lambda_
1✔
1389
    logs_client = aws_client.logs
1✔
1390
    s3_client = aws_client.s3
1✔
1391

1392
    def _create_lambda_function(*args, **kwargs):
1✔
1393
        client = kwargs.get("client") or lambda_client
1✔
1394
        kwargs["client"] = client
1✔
1395
        kwargs["s3_client"] = s3_client
1✔
1396
        func_name = kwargs.get("func_name")
1✔
1397
        assert func_name
1✔
1398
        del kwargs["func_name"]
1✔
1399

1400
        if not kwargs.get("role"):
1✔
1401
            kwargs["role"] = lambda_su_role
1✔
1402

1403
        def _create_function():
1✔
1404
            resp = testutil.create_lambda_function(func_name, **kwargs)
1✔
1405
            lambda_arns_and_clients.append((resp["CreateFunctionResponse"]["FunctionArn"], client))
1✔
1406
            wait_until_lambda_ready(function_name=func_name, client=client)
1✔
1407
            log_group_name = f"/aws/lambda/{func_name}"
1✔
1408
            log_groups.append(log_group_name)
1✔
1409
            return resp
1✔
1410

1411
        # @AWS, takes about 10s until the role/policy is "active", until then it will fail
1412
        # localstack should normally not require the retries and will just continue here
1413
        return retry(_create_function, retries=3, sleep=4)
1✔
1414

1415
    yield _create_lambda_function
1✔
1416

1417
    for arn, client in lambda_arns_and_clients:
1✔
1418
        try:
1✔
1419
            client.delete_function(FunctionName=arn)
1✔
1420
        except Exception:
1✔
1421
            LOG.debug("Unable to delete function arn=%s in cleanup", arn)
1✔
1422

1423
    for log_group_name in log_groups:
1✔
1424
        try:
1✔
1425
            logs_client.delete_log_group(logGroupName=log_group_name)
1✔
1426
        except Exception:
1✔
1427
            LOG.debug("Unable to delete log group %s in cleanup", log_group_name)
1✔
1428

1429

1430
@pytest.fixture
1✔
1431
def lambda_is_function_deleted(aws_client):
1✔
1432
    """Example usage:
1433
    wait_until(lambda_is_function_deleted(function_name))
1434
    wait_until(lambda_is_function_deleted(function_name, Qualifier="my-alias"))
1435

1436
    function_name can be a function name, function ARN, or partial function ARN.
1437
    """
1438
    return _lambda_is_function_deleted(aws_client.lambda_)
×
1439

1440

1441
def _lambda_is_function_deleted(lambda_client):
1✔
1442
    def _is_function_deleted(
×
1443
        function_name: str,
1444
        **kwargs,
1445
    ) -> Callable[[], bool]:
1446
        def _inner() -> bool:
×
1447
            try:
×
1448
                lambda_client.get_function(FunctionName=function_name, **kwargs)
×
1449
                return False
×
1450
            except lambda_client.exceptions.ResourceNotFoundException:
×
1451
                return True
×
1452

1453
        return _inner
×
1454

1455
    return _is_function_deleted
×
1456

1457

1458
@pytest.fixture
1✔
1459
def create_echo_http_server(aws_client, create_lambda_function):
1✔
1460
    from localstack.aws.api.lambda_ import Runtime
1✔
1461

1462
    lambda_client = aws_client.lambda_
1✔
1463
    handler_code = textwrap.dedent(
1✔
1464
        """
1465
    import json
1466
    import os
1467

1468

1469
    def make_response(body: dict, status_code: int = 200):
1470
        return {
1471
            "statusCode": status_code,
1472
            "headers": {"Content-Type": "application/json"},
1473
            "body": body,
1474
        }
1475

1476

1477
    def trim_headers(headers):
1478
        if not int(os.getenv("TRIM_X_HEADERS", 0)):
1479
            return headers
1480
        return {
1481
            key: value for key, value in headers.items()
1482
            if not (key.startswith("x-amzn") or key.startswith("x-forwarded-"))
1483
        }
1484

1485

1486
    def handler(event, context):
1487
        print(json.dumps(event))
1488
        response = {
1489
            "args": event.get("queryStringParameters", {}),
1490
            "data": event.get("body", ""),
1491
            "domain": event["requestContext"].get("domainName", ""),
1492
            "headers": trim_headers(event.get("headers", {})),
1493
            "method": event["requestContext"]["http"].get("method", ""),
1494
            "origin": event["requestContext"]["http"].get("sourceIp", ""),
1495
            "path": event["requestContext"]["http"].get("path", ""),
1496
        }
1497
        return make_response(response)"""
1498
    )
1499

1500
    def _create_echo_http_server(trim_x_headers: bool = False) -> str:
1✔
1501
        """Creates a server that will echo any request. Any request will be returned with the
1502
        following format. Any unset values will have those defaults.
1503
        `trim_x_headers` can be set to True to trim some headers that are automatically added by lambda in
1504
        order to create easier Snapshot testing. Default: `False`
1505
        {
1506
          "args": {},
1507
          "headers": {},
1508
          "data": "",
1509
          "method": "",
1510
          "domain": "",
1511
          "origin": "",
1512
          "path": ""
1513
        }"""
1514
        zip_file = testutil.create_lambda_archive(handler_code, get_content=True)
1✔
1515
        func_name = f"echo-http-{short_uid()}"
1✔
1516
        create_lambda_function(
1✔
1517
            func_name=func_name,
1518
            zip_file=zip_file,
1519
            runtime=Runtime.python3_12,
1520
            envvars={"TRIM_X_HEADERS": "1" if trim_x_headers else "0"},
1521
        )
1522
        url_response = lambda_client.create_function_url_config(
1✔
1523
            FunctionName=func_name, AuthType="NONE"
1524
        )
1525
        aws_client.lambda_.add_permission(
1✔
1526
            FunctionName=func_name,
1527
            StatementId="urlPermission",
1528
            Action="lambda:InvokeFunctionUrl",
1529
            Principal="*",
1530
            FunctionUrlAuthType="NONE",
1531
        )
1532
        return url_response["FunctionUrl"]
1✔
1533

1534
    yield _create_echo_http_server
1✔
1535

1536

1537
@pytest.fixture
1✔
1538
def create_event_source_mapping(aws_client):
1✔
1539
    uuids = []
1✔
1540

1541
    def _create_event_source_mapping(*args, **kwargs):
1✔
1542
        response = aws_client.lambda_.create_event_source_mapping(*args, **kwargs)
1✔
1543
        uuids.append(response["UUID"])
1✔
1544
        return response
1✔
1545

1546
    yield _create_event_source_mapping
1✔
1547

1548
    for uuid in uuids:
1✔
1549
        try:
1✔
1550
            aws_client.lambda_.delete_event_source_mapping(UUID=uuid)
1✔
1551
        except aws_client.lambda_.exceptions.ResourceNotFoundException:
1✔
1552
            pass
1✔
1553
        except Exception as ex:
×
1554
            LOG.debug("Unable to delete event source mapping %s in cleanup: %s", uuid, ex)
×
1555

1556

1557
@pytest.fixture
1✔
1558
def check_lambda_logs(aws_client):
1✔
1559
    def _check_logs(func_name: str, expected_lines: list[str] = None) -> list[str]:
1✔
1560
        if not expected_lines:
1✔
1561
            expected_lines = []
×
1562
        log_events = get_lambda_logs(func_name, logs_client=aws_client.logs)
1✔
1563
        log_messages = [e["message"] for e in log_events]
1✔
1564
        for line in expected_lines:
1✔
1565
            if ".*" in line:
1✔
1566
                found = [re.match(line, m, flags=re.DOTALL) for m in log_messages]
1✔
1567
                if any(found):
1✔
1568
                    continue
1✔
1569
            assert line in log_messages
×
1570
        return log_messages
1✔
1571

1572
    return _check_logs
1✔
1573

1574

1575
@pytest.fixture
1✔
1576
def create_policy(aws_client):
1✔
1577
    policy_arns = []
1✔
1578

1579
    def _create_policy(*args, iam_client=None, **kwargs):
1✔
1580
        iam_client = iam_client or aws_client.iam
1✔
1581
        if "PolicyName" not in kwargs:
1✔
1582
            kwargs["PolicyName"] = f"policy-{short_uid()}"
1✔
1583
        response = iam_client.create_policy(*args, **kwargs)
1✔
1584
        policy_arn = response["Policy"]["Arn"]
1✔
1585
        policy_arns.append((policy_arn, iam_client))
1✔
1586
        return response
1✔
1587

1588
    yield _create_policy
1✔
1589

1590
    for policy_arn, iam_client in policy_arns:
1✔
1591
        try:
1✔
1592
            iam_client.delete_policy(PolicyArn=policy_arn)
1✔
1593
        except Exception:
1✔
1594
            LOG.debug("Could not delete policy '%s' during test cleanup", policy_arn)
1✔
1595

1596

1597
@pytest.fixture
1✔
1598
def create_user(aws_client):
1✔
1599
    usernames = []
1✔
1600

1601
    def _create_user(**kwargs):
1✔
1602
        if "UserName" not in kwargs:
1✔
1603
            kwargs["UserName"] = f"user-{short_uid()}"
×
1604
        response = aws_client.iam.create_user(**kwargs)
1✔
1605
        usernames.append(response["User"]["UserName"])
1✔
1606
        return response
1✔
1607

1608
    yield _create_user
1✔
1609

1610
    for username in usernames:
1✔
1611
        try:
1✔
1612
            inline_policies = aws_client.iam.list_user_policies(UserName=username)["PolicyNames"]
1✔
1613
        except ClientError as e:
1✔
1614
            LOG.debug(
1✔
1615
                "Cannot list user policies: %s. User %s probably already deleted...", e, username
1616
            )
1617
            continue
1✔
1618

1619
        for inline_policy in inline_policies:
1✔
1620
            try:
1✔
1621
                aws_client.iam.delete_user_policy(UserName=username, PolicyName=inline_policy)
1✔
1622
            except Exception:
×
1623
                LOG.debug(
×
1624
                    "Could not delete user policy '%s' from '%s' during cleanup",
1625
                    inline_policy,
1626
                    username,
1627
                )
1628
        attached_policies = aws_client.iam.list_attached_user_policies(UserName=username)[
1✔
1629
            "AttachedPolicies"
1630
        ]
1631
        for attached_policy in attached_policies:
1✔
1632
            try:
1✔
1633
                aws_client.iam.detach_user_policy(
1✔
1634
                    UserName=username, PolicyArn=attached_policy["PolicyArn"]
1635
                )
1636
            except Exception:
1✔
1637
                LOG.debug(
1✔
1638
                    "Error detaching policy '%s' from user '%s'",
1639
                    attached_policy["PolicyArn"],
1640
                    username,
1641
                )
1642
        access_keys = aws_client.iam.list_access_keys(UserName=username)["AccessKeyMetadata"]
1✔
1643
        for access_key in access_keys:
1✔
1644
            try:
1✔
1645
                aws_client.iam.delete_access_key(
1✔
1646
                    UserName=username, AccessKeyId=access_key["AccessKeyId"]
1647
                )
1648
            except Exception:
×
1649
                LOG.debug(
×
1650
                    "Error deleting access key '%s' from user '%s'",
1651
                    access_key["AccessKeyId"],
1652
                    username,
1653
                )
1654

1655
        try:
1✔
1656
            aws_client.iam.delete_user(UserName=username)
1✔
1657
        except Exception as e:
1✔
1658
            LOG.debug("Error deleting user '%s' during test cleanup: %s", username, e)
1✔
1659

1660

1661
@pytest.fixture
1✔
1662
def wait_and_assume_role(aws_client):
1✔
1663
    def _wait_and_assume_role(role_arn: str, session_name: str = None, **kwargs):
1✔
1664
        if not session_name:
1✔
1665
            session_name = f"session-{short_uid()}"
1✔
1666

1667
        def assume_role():
1✔
1668
            return aws_client.sts.assume_role(
1✔
1669
                RoleArn=role_arn, RoleSessionName=session_name, **kwargs
1670
            )["Credentials"]
1671

1672
        # need to retry a couple of times before we are allowed to assume this role in AWS
1673
        keys = retry(assume_role, sleep=5, retries=4)
1✔
1674
        return keys
1✔
1675

1676
    return _wait_and_assume_role
1✔
1677

1678

1679
@pytest.fixture
1✔
1680
def create_role(aws_client):
1✔
1681
    role_names = []
1✔
1682

1683
    def _create_role(iam_client=None, **kwargs):
1✔
1684
        if not kwargs.get("RoleName"):
1✔
1685
            kwargs["RoleName"] = f"role-{short_uid()}"
×
1686
        iam_client = iam_client or aws_client.iam
1✔
1687
        result = iam_client.create_role(**kwargs)
1✔
1688
        role_names.append((result["Role"]["RoleName"], iam_client))
1✔
1689
        return result
1✔
1690

1691
    yield _create_role
1✔
1692

1693
    for role_name, iam_client in role_names:
1✔
1694
        # detach policies
1695
        try:
1✔
1696
            attached_policies = iam_client.list_attached_role_policies(RoleName=role_name)[
1✔
1697
                "AttachedPolicies"
1698
            ]
1699
        except ClientError as e:
1✔
1700
            LOG.debug(
1✔
1701
                "Cannot list attached role policies: %s. Role %s probably already deleted...",
1702
                e,
1703
                role_name,
1704
            )
1705
            continue
1✔
1706
        for attached_policy in attached_policies:
1✔
1707
            try:
1✔
1708
                iam_client.detach_role_policy(
1✔
1709
                    RoleName=role_name, PolicyArn=attached_policy["PolicyArn"]
1710
                )
1711
            except Exception:
×
1712
                LOG.debug(
×
1713
                    "Could not detach role policy '%s' from '%s' during cleanup",
1714
                    attached_policy["PolicyArn"],
1715
                    role_name,
1716
                )
1717
        role_policies = iam_client.list_role_policies(RoleName=role_name)["PolicyNames"]
1✔
1718
        for role_policy in role_policies:
1✔
1719
            try:
1✔
1720
                iam_client.delete_role_policy(RoleName=role_name, PolicyName=role_policy)
1✔
1721
            except Exception:
×
1722
                LOG.debug(
×
1723
                    "Could not delete role policy '%s' from '%s' during cleanup",
1724
                    role_policy,
1725
                    role_name,
1726
                )
1727
        try:
1✔
1728
            iam_client.delete_role(RoleName=role_name)
1✔
1729
        except Exception:
×
1730
            LOG.debug("Could not delete role '%s' during cleanup", role_name)
×
1731

1732

1733
@pytest.fixture
1✔
1734
def create_parameter(aws_client):
1✔
1735
    params = []
1✔
1736

1737
    def _create_parameter(**kwargs):
1✔
1738
        params.append(kwargs["Name"])
1✔
1739
        return aws_client.ssm.put_parameter(**kwargs)
1✔
1740

1741
    yield _create_parameter
1✔
1742

1743
    for param in params:
1✔
1744
        aws_client.ssm.delete_parameter(Name=param)
1✔
1745

1746

1747
@pytest.fixture
1✔
1748
def create_secret(aws_client):
1✔
1749
    items = []
1✔
1750

1751
    def _create_parameter(**kwargs):
1✔
1752
        create_response = aws_client.secretsmanager.create_secret(**kwargs)
1✔
1753
        items.append(create_response["ARN"])
1✔
1754
        return create_response
1✔
1755

1756
    yield _create_parameter
1✔
1757

1758
    for item in items:
1✔
1759
        aws_client.secretsmanager.delete_secret(SecretId=item, ForceDeleteWithoutRecovery=True)
1✔
1760

1761

1762
# TODO Figure out how to make cert creation tests pass against AWS.
1763
#
1764
# We would like to have localstack tests to pass not just against localstack, but also against AWS to make sure
1765
# our emulation is correct. Unfortunately, with certificate creation there are some issues.
1766
#
1767
# In AWS newly created ACM certificates have to be validated either by email or by DNS. The latter is
1768
# by adding some CNAME records as requested by ASW in response to a certificate request.
1769
# For testing purposes the DNS one seems to be easier, at least as long as DNS is handled by Region53 AWS DNS service.
1770
#
1771
# The other possible option is to use IAM certificates instead of ACM ones. Those just have to be uploaded from files
1772
# created by openssl etc. Not sure if there are other issues after that.
1773
#
1774
# The third option might be having in AWS some certificates created in advance - so they do not require validation
1775
# and can be easily used in tests. The issie with such an approach is that for AppSync, for example, in order to
1776
# register a domain name (https://docs.aws.amazon.com/appsync/latest/APIReference/API_CreateDomainName.html),
1777
# the domain name in the API request has to match the domain name used in certificate creation. Which means that with
1778
# pre-created certificates we would have to use specific domain names instead of random ones.
1779
@pytest.fixture
1✔
1780
def acm_request_certificate(aws_client_factory):
1✔
1781
    certificate_arns = []
1✔
1782

1783
    def factory(**kwargs) -> str:
1✔
1784
        if "DomainName" not in kwargs:
1✔
1785
            kwargs["DomainName"] = f"test-domain-{short_uid()}.localhost.localstack.cloud"
1✔
1786

1787
        region_name = kwargs.pop("region_name", None)
1✔
1788
        acm_client = aws_client_factory(region_name=region_name).acm
1✔
1789

1790
        response = acm_client.request_certificate(**kwargs)
1✔
1791
        created_certificate_arn = response["CertificateArn"]
1✔
1792
        certificate_arns.append((created_certificate_arn, region_name))
1✔
1793
        return response
1✔
1794

1795
    yield factory
1✔
1796

1797
    # cleanup
1798
    for certificate_arn, region_name in certificate_arns:
1✔
1799
        try:
1✔
1800
            acm_client = aws_client_factory(region_name=region_name).acm
1✔
1801
            acm_client.delete_certificate(CertificateArn=certificate_arn)
1✔
1802
        except Exception as e:
×
1803
            LOG.debug("error cleaning up certificate %s: %s", certificate_arn, e)
×
1804

1805

1806
role_policy_su = {
1✔
1807
    "Version": "2012-10-17",
1808
    "Statement": [{"Effect": "Allow", "Action": ["*"], "Resource": ["*"]}],
1809
}
1810

1811

1812
@pytest.fixture(scope="session")
1✔
1813
def lambda_su_role(aws_client):
1✔
1814
    role_name = f"lambda-autogenerated-{short_uid()}"
1✔
1815
    role = aws_client.iam.create_role(
1✔
1816
        RoleName=role_name, AssumeRolePolicyDocument=role_assume_policy
1817
    )["Role"]
1818
    policy_name = f"lambda-autogenerated-{short_uid()}"
1✔
1819
    policy_arn = aws_client.iam.create_policy(
1✔
1820
        PolicyName=policy_name, PolicyDocument=json.dumps(role_policy_su)
1821
    )["Policy"]["Arn"]
1822
    aws_client.iam.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
1✔
1823

1824
    if is_aws_cloud():  # dirty but necessary
1✔
1825
        time.sleep(10)
×
1826

1827
    yield role["Arn"]
1✔
1828

1829
    run_safe(aws_client.iam.detach_role_policy(RoleName=role_name, PolicyArn=policy_arn))
1✔
1830
    run_safe(aws_client.iam.delete_role(RoleName=role_name))
1✔
1831
    run_safe(aws_client.iam.delete_policy(PolicyArn=policy_arn))
1✔
1832

1833

1834
@pytest.fixture
1✔
1835
def create_iam_role_and_attach_policy(aws_client):
1✔
1836
    """
1837
    Fixture that creates an IAM role with given role definition and predefined policy ARN.
1838

1839
    Use this fixture with AWS managed policies like 'AmazonS3ReadOnlyAccess' or 'AmazonKinesisFullAccess'.
1840
    """
1841
    roles = []
×
1842

1843
    def _inner(**kwargs: dict[str, any]) -> str:
×
1844
        """
1845
        :param dict RoleDefinition: role definition document
1846
        :param str PolicyArn: policy ARN
1847
        :param str RoleName: role name (autogenerated if omitted)
1848
        :return: role ARN
1849
        """
1850
        if "RoleName" not in kwargs:
×
1851
            kwargs["RoleName"] = f"test-role-{short_uid()}"
×
1852

1853
        role = kwargs["RoleName"]
×
1854
        role_policy = json.dumps(kwargs["RoleDefinition"])
×
1855

1856
        result = aws_client.iam.create_role(RoleName=role, AssumeRolePolicyDocument=role_policy)
×
1857
        role_arn = result["Role"]["Arn"]
×
1858

1859
        policy_arn = kwargs["PolicyArn"]
×
1860
        aws_client.iam.attach_role_policy(PolicyArn=policy_arn, RoleName=role)
×
1861

1862
        roles.append(role)
×
1863
        return role_arn
×
1864

1865
    yield _inner
×
1866

1867
    for role in roles:
×
1868
        try:
×
1869
            aws_client.iam.delete_role(RoleName=role)
×
1870
        except Exception as exc:
×
1871
            LOG.debug("Error deleting IAM role '%s': %s", role, exc)
×
1872

1873

1874
@pytest.fixture
1✔
1875
def create_iam_role_with_policy(aws_client):
1✔
1876
    """
1877
    Fixture that creates an IAM role with given role definition and policy definition.
1878
    """
1879
    roles = {}
1✔
1880

1881
    def _create_role_and_policy(**kwargs: dict[str, any]) -> str:
1✔
1882
        """
1883
        :param dict RoleDefinition: role definition document
1884
        :param dict PolicyDefinition: policy definition document
1885
        :param str PolicyName: policy name (autogenerated if omitted)
1886
        :param str RoleName: role name (autogenerated if omitted)
1887
        :return: role ARN
1888
        """
1889
        if "RoleName" not in kwargs:
1✔
1890
            kwargs["RoleName"] = f"test-role-{short_uid()}"
1✔
1891
        role = kwargs["RoleName"]
1✔
1892
        if "PolicyName" not in kwargs:
1✔
1893
            kwargs["PolicyName"] = f"test-policy-{short_uid()}"
1✔
1894
        policy = kwargs["PolicyName"]
1✔
1895
        role_policy = json.dumps(kwargs["RoleDefinition"])
1✔
1896

1897
        result = aws_client.iam.create_role(RoleName=role, AssumeRolePolicyDocument=role_policy)
1✔
1898
        role_arn = result["Role"]["Arn"]
1✔
1899

1900
        policy_document = json.dumps(kwargs["PolicyDefinition"])
1✔
1901
        aws_client.iam.put_role_policy(
1✔
1902
            RoleName=role, PolicyName=policy, PolicyDocument=policy_document
1903
        )
1904
        roles[role] = policy
1✔
1905
        return role_arn
1✔
1906

1907
    yield _create_role_and_policy
1✔
1908

1909
    for role_name, policy_name in roles.items():
1✔
1910
        try:
1✔
1911
            aws_client.iam.delete_role_policy(RoleName=role_name, PolicyName=policy_name)
1✔
1912
        except Exception as exc:
×
1913
            LOG.debug("Error deleting IAM role policy '%s' '%s': %s", role_name, policy_name, exc)
×
1914
        try:
1✔
1915
            aws_client.iam.delete_role(RoleName=role_name)
1✔
1916
        except Exception as exc:
×
1917
            LOG.debug("Error deleting IAM role '%s': %s", role_name, exc)
×
1918

1919

1920
@pytest.fixture
1✔
1921
def firehose_create_delivery_stream(wait_for_delivery_stream_ready, aws_client):
1✔
1922
    delivery_stream_names = []
1✔
1923

1924
    def _create_delivery_stream(**kwargs):
1✔
1925
        if "DeliveryStreamName" not in kwargs:
1✔
1926
            kwargs["DeliveryStreamName"] = f"test-delivery-stream-{short_uid()}"
×
1927
        delivery_stream_name = kwargs["DeliveryStreamName"]
1✔
1928
        response = aws_client.firehose.create_delivery_stream(**kwargs)
1✔
1929
        delivery_stream_names.append(delivery_stream_name)
1✔
1930
        wait_for_delivery_stream_ready(delivery_stream_name)
1✔
1931
        return response
1✔
1932

1933
    yield _create_delivery_stream
1✔
1934

1935
    for delivery_stream_name in delivery_stream_names:
1✔
1936
        try:
1✔
1937
            aws_client.firehose.delete_delivery_stream(DeliveryStreamName=delivery_stream_name)
1✔
1938
        except Exception:
×
1939
            LOG.info("Failed to delete delivery stream %s", delivery_stream_name)
×
1940

1941

1942
@pytest.fixture
1✔
1943
def ses_configuration_set(aws_client):
1✔
1944
    configuration_set_names = []
1✔
1945

1946
    def factory(name: str) -> None:
1✔
1947
        aws_client.ses.create_configuration_set(
1✔
1948
            ConfigurationSet={
1949
                "Name": name,
1950
            },
1951
        )
1952
        configuration_set_names.append(name)
1✔
1953

1954
    yield factory
1✔
1955

1956
    for configuration_set_name in configuration_set_names:
1✔
1957
        aws_client.ses.delete_configuration_set(ConfigurationSetName=configuration_set_name)
1✔
1958

1959

1960
@pytest.fixture
1✔
1961
def ses_configuration_set_sns_event_destination(aws_client):
1✔
1962
    event_destinations = []
1✔
1963

1964
    def factory(config_set_name: str, event_destination_name: str, topic_arn: str) -> None:
1✔
1965
        aws_client.ses.create_configuration_set_event_destination(
1✔
1966
            ConfigurationSetName=config_set_name,
1967
            EventDestination={
1968
                "Name": event_destination_name,
1969
                "Enabled": True,
1970
                "MatchingEventTypes": ["send", "bounce", "delivery", "open", "click"],
1971
                "SNSDestination": {
1972
                    "TopicARN": topic_arn,
1973
                },
1974
            },
1975
        )
1976
        event_destinations.append((config_set_name, event_destination_name))
1✔
1977

1978
    yield factory
1✔
1979

1980
    for created_config_set_name, created_event_destination_name in event_destinations:
1✔
1981
        aws_client.ses.delete_configuration_set_event_destination(
1✔
1982
            ConfigurationSetName=created_config_set_name,
1983
            EventDestinationName=created_event_destination_name,
1984
        )
1985

1986

1987
@pytest.fixture
1✔
1988
def ses_email_template(aws_client):
1✔
1989
    template_names = []
1✔
1990

1991
    def factory(name: str, contents: str, subject: str = f"Email template {short_uid()}"):
1✔
1992
        aws_client.ses.create_template(
1✔
1993
            Template={
1994
                "TemplateName": name,
1995
                "SubjectPart": subject,
1996
                "TextPart": contents,
1997
            }
1998
        )
1999
        template_names.append(name)
1✔
2000

2001
    yield factory
1✔
2002

2003
    for template_name in template_names:
1✔
2004
        aws_client.ses.delete_template(TemplateName=template_name)
1✔
2005

2006

2007
@pytest.fixture
1✔
2008
def ses_verify_identity(aws_client):
1✔
2009
    identities = []
1✔
2010

2011
    def factory(email_address: str) -> None:
1✔
2012
        aws_client.ses.verify_email_identity(EmailAddress=email_address)
1✔
2013

2014
    yield factory
1✔
2015

2016
    for identity in identities:
1✔
2017
        aws_client.ses.delete_identity(Identity=identity)
×
2018

2019

2020
@pytest.fixture
1✔
2021
def setup_sender_email_address(ses_verify_identity):
1✔
2022
    """
2023
    If the test is running against AWS then assume the email address passed is already
2024
    verified, and passes the given email address through. Otherwise, it generates one random
2025
    email address and verify them.
2026
    """
2027

2028
    def inner(sender_email_address: str | None = None) -> str:
1✔
2029
        if is_aws_cloud():
1✔
2030
            if sender_email_address is None:
×
2031
                raise ValueError(
×
2032
                    "sender_email_address must be specified to run this test against AWS"
2033
                )
2034
        else:
2035
            # overwrite the given parameters with localstack specific ones
2036
            sender_email_address = f"sender-{short_uid()}@example.com"
1✔
2037
            ses_verify_identity(sender_email_address)
1✔
2038

2039
        return sender_email_address
1✔
2040

2041
    return inner
1✔
2042

2043

2044
@pytest.fixture
1✔
2045
def ec2_create_security_group(aws_client):
1✔
2046
    ec2_sgs = []
1✔
2047

2048
    def factory(ports=None, ip_protocol: str = "tcp", **kwargs):
1✔
2049
        """
2050
        Create the target group and authorize the security group ingress.
2051
        :param ports: list of ports to be authorized for the ingress rule.
2052
        :param ip_protocol: the ip protocol for the permissions (tcp by default)
2053
        """
2054
        if "GroupName" not in kwargs:
1✔
2055
            # FIXME: This will fail against AWS since the sg prefix is not valid for GroupName
2056
            # > "Group names may not be in the format sg-*".
2057
            kwargs["GroupName"] = f"sg-{short_uid()}"
×
2058
        # Making sure the call to CreateSecurityGroup gets the right arguments
2059
        _args = select_from_typed_dict(CreateSecurityGroupRequest, kwargs)
1✔
2060
        security_group = aws_client.ec2.create_security_group(**_args)
1✔
2061
        security_group_id = security_group["GroupId"]
1✔
2062

2063
        # FIXME: If 'ports' is None or an empty list, authorize_security_group_ingress will fail due to missing IpPermissions.
2064
        # Must ensure ports are explicitly provided or skip authorization entirely if not required.
2065
        permissions = [
1✔
2066
            {
2067
                "FromPort": port,
2068
                "IpProtocol": ip_protocol,
2069
                "IpRanges": [{"CidrIp": "0.0.0.0/0"}],
2070
                "ToPort": port,
2071
            }
2072
            for port in ports or []
2073
        ]
2074
        aws_client.ec2.authorize_security_group_ingress(
1✔
2075
            GroupId=security_group_id,
2076
            IpPermissions=permissions,
2077
        )
2078

2079
        ec2_sgs.append(security_group_id)
1✔
2080
        return security_group
1✔
2081

2082
    yield factory
1✔
2083

2084
    for sg_group_id in ec2_sgs:
1✔
2085
        try:
1✔
2086
            aws_client.ec2.delete_security_group(GroupId=sg_group_id)
1✔
2087
        except Exception as e:
×
2088
            LOG.debug("Error cleaning up EC2 security group: %s, %s", sg_group_id, e)
×
2089

2090

2091
@pytest.fixture
1✔
2092
def ec2_create_vpc_endpoint(aws_client):
1✔
2093
    vpc_endpoints = []
1✔
2094

2095
    def _create(**kwargs: Unpack[CreateVpcEndpointRequest]) -> VpcEndpoint:
1✔
2096
        endpoint = aws_client.ec2.create_vpc_endpoint(**kwargs)
1✔
2097
        endpoint_id = endpoint["VpcEndpoint"]["VpcEndpointId"]
1✔
2098
        vpc_endpoints.append(endpoint_id)
1✔
2099

2100
        def _check_available() -> VpcEndpoint:
1✔
2101
            result = aws_client.ec2.describe_vpc_endpoints(VpcEndpointIds=[endpoint_id])
1✔
2102
            _endpoint_details = result["VpcEndpoints"][0]
1✔
2103
            assert _endpoint_details["State"] == "available"
1✔
2104

2105
            return _endpoint_details
1✔
2106

2107
        return retry(_check_available, retries=30, sleep=5 if is_aws_cloud() else 1)
1✔
2108

2109
    yield _create
1✔
2110

2111
    try:
1✔
2112
        aws_client.ec2.delete_vpc_endpoints(VpcEndpointIds=vpc_endpoints)
1✔
2113
    except Exception as e:
×
2114
        LOG.error("Error cleaning up VPC endpoint: %s, %s", vpc_endpoints, e)
×
2115

2116
    def wait_for_endpoint_deleted():
1✔
2117
        try:
×
2118
            endpoints = aws_client.ec2.describe_vpc_endpoints(VpcEndpointIds=vpc_endpoints)
×
2119
            assert len(endpoints["VpcEndpoints"]) == 0 or all(
×
2120
                endpoint["State"] == "Deleted" for endpoint in endpoints["VpcEndpoints"]
2121
            )
2122
        except botocore.exceptions.ClientError:
×
2123
            pass
×
2124

2125
    # the vpc can't be deleted if an endpoint exists
2126
    if is_aws_cloud():
1✔
2127
        retry(wait_for_endpoint_deleted, retries=30, sleep=10 if is_aws_cloud() else 1)
×
2128

2129

2130
@pytest.fixture
1✔
2131
def cleanups():
1✔
2132
    cleanup_fns = []
1✔
2133

2134
    yield cleanup_fns
1✔
2135

2136
    for cleanup_callback in cleanup_fns[::-1]:
1✔
2137
        try:
1✔
2138
            cleanup_callback()
1✔
2139
        except ClientError as e:
1✔
2140
            http_code = e.response["ResponseMetadata"]["HTTPStatusCode"]
1✔
2141
            # Covers non-standardized error codes such as NotFoundException, NoSuchEntity (IAM), NoSuchBucket (S3), etc
2142
            if http_code == 404:
1✔
2143
                LOG.warning(
1✔
2144
                    "Failed to execute cleanup because a resource was not found. "
2145
                    "This cleanup might be unnecessary. %s",
2146
                    str(e),
2147
                    exc_info=e,
2148
                )
2149
            else:
2150
                LOG.warning(
1✔
2151
                    "Failed to execute cleanup due to ClientError: %s",
2152
                    str(e),
2153
                    exc_info=e,
2154
                )
2155
        except Exception as e:
1✔
2156
            LOG.warning(
1✔
2157
                "Failed to execute cleanup due to unexpected error: %s",
2158
                str(e),
2159
                exc_info=e,
2160
            )
2161

2162

2163
@pytest.fixture(scope="session")
1✔
2164
def account_id(aws_client):
1✔
2165
    if is_aws_cloud() or is_api_enabled("sts"):
1✔
2166
        return aws_client.sts.get_caller_identity()["Account"]
1✔
2167
    else:
2168
        return TEST_AWS_ACCOUNT_ID
×
2169

2170

2171
@pytest.fixture(scope="session")
1✔
2172
def region_name(aws_client):
1✔
2173
    if is_aws_cloud() or is_api_enabled("sts"):
1✔
2174
        return aws_client.sts.meta.region_name
1✔
2175
    else:
2176
        return TEST_AWS_REGION_NAME
×
2177

2178

2179
@pytest.fixture(scope="session")
1✔
2180
def partition(region_name):
1✔
2181
    return get_partition(region_name)
1✔
2182

2183

2184
@pytest.fixture(scope="session")
1✔
2185
def secondary_account_id(secondary_aws_client):
1✔
2186
    if is_aws_cloud() or is_api_enabled("sts"):
1✔
2187
        return secondary_aws_client.sts.get_caller_identity()["Account"]
1✔
2188
    else:
2189
        return SECONDARY_TEST_AWS_ACCOUNT_ID
×
2190

2191

2192
@pytest.fixture(scope="session")
1✔
2193
def secondary_region_name():
1✔
2194
    return SECONDARY_TEST_AWS_REGION_NAME
1✔
2195

2196

2197
@pytest.hookimpl
1✔
2198
def pytest_collection_modifyitems(config: Config, items: list[Item]):
1✔
2199
    only_localstack = pytest.mark.skipif(
1✔
2200
        is_aws_cloud(),
2201
        reason="test only applicable if run against localstack",
2202
    )
2203
    for item in items:
1✔
2204
        for mark in item.iter_markers():
1✔
2205
            if mark.name.endswith("only_localstack"):
1✔
2206
                item.add_marker(only_localstack)
1✔
2207
        if hasattr(item, "fixturenames") and "snapshot" in item.fixturenames:
1✔
2208
            # add a marker that indicates that this test is snapshot validated
2209
            # if it uses the snapshot fixture -> allows selecting only snapshot
2210
            # validated tests in order to capture new snapshots for a whole
2211
            # test file with "-m snapshot_validated"
2212
            item.add_marker("snapshot_validated")
1✔
2213

2214

2215
@pytest.fixture
1✔
2216
def sample_stores() -> AccountRegionBundle:
1✔
2217
    class SampleStore(BaseStore):
1✔
2218
        CROSS_ACCOUNT_ATTR = CrossAccountAttribute(default=list)
1✔
2219
        CROSS_REGION_ATTR = CrossRegionAttribute(default=list)
1✔
2220
        region_specific_attr = LocalAttribute(default=list)
1✔
2221

2222
    return AccountRegionBundle("zzz", SampleStore, validate=False)
1✔
2223

2224

2225
@pytest.fixture
1✔
2226
def create_rest_apigw(aws_client_factory):
1✔
2227
    rest_apis = []
1✔
2228
    retry_boto_config = None
1✔
2229
    if is_aws_cloud():
1✔
2230
        retry_boto_config = botocore.config.Config(
×
2231
            # Api gateway can throttle requests pretty heavily. Leading to potentially undeleted apis
2232
            retries={"max_attempts": 10, "mode": "adaptive"}
2233
        )
2234

2235
    def _create_apigateway_function(**kwargs):
1✔
2236
        client_region_name = kwargs.pop("region_name", None)
1✔
2237
        apigateway_client = aws_client_factory(
1✔
2238
            region_name=client_region_name, config=retry_boto_config
2239
        ).apigateway
2240
        kwargs.setdefault("name", f"api-{short_uid()}")
1✔
2241

2242
        response = apigateway_client.create_rest_api(**kwargs)
1✔
2243
        api_id = response.get("id")
1✔
2244
        rest_apis.append((api_id, client_region_name))
1✔
2245

2246
        return api_id, response.get("name"), response.get("rootResourceId")
1✔
2247

2248
    yield _create_apigateway_function
1✔
2249

2250
    for rest_api_id, _client_region_name in rest_apis:
1✔
2251
        apigateway_client = aws_client_factory(
1✔
2252
            region_name=_client_region_name,
2253
            config=retry_boto_config,
2254
        ).apigateway
2255
        # First, retrieve the usage plans associated with the REST API
2256
        usage_plan_ids = []
1✔
2257
        usage_plans = apigateway_client.get_usage_plans()
1✔
2258
        for item in usage_plans.get("items", []):
1✔
2259
            api_stages = item.get("apiStages", [])
1✔
2260
            usage_plan_ids.extend(
1✔
2261
                item.get("id") for api_stage in api_stages if api_stage.get("apiId") == rest_api_id
2262
            )
2263

2264
        # Then delete the API, as you can't delete the UsagePlan if a stage is associated with it
2265
        with contextlib.suppress(Exception):
1✔
2266
            apigateway_client.delete_rest_api(restApiId=rest_api_id)
1✔
2267

2268
        # finally delete the usage plans and the API Keys linked to it
2269
        for usage_plan_id in usage_plan_ids:
1✔
2270
            usage_plan_keys = apigateway_client.get_usage_plan_keys(usagePlanId=usage_plan_id)
1✔
2271
            for key in usage_plan_keys.get("items", []):
1✔
2272
                apigateway_client.delete_api_key(apiKey=key["id"])
×
2273
            apigateway_client.delete_usage_plan(usagePlanId=usage_plan_id)
1✔
2274

2275

2276
@pytest.fixture
1✔
2277
def create_rest_apigw_openapi(aws_client_factory):
1✔
2278
    rest_apis = []
×
2279

2280
    def _create_apigateway_function(**kwargs):
×
2281
        region_name = kwargs.pop("region_name", None)
×
2282
        apigateway_client = aws_client_factory(region_name=region_name).apigateway
×
2283

2284
        response = apigateway_client.import_rest_api(**kwargs)
×
2285
        api_id = response.get("id")
×
2286
        rest_apis.append((api_id, region_name))
×
2287
        return api_id, response
×
2288

2289
    yield _create_apigateway_function
×
2290

2291
    for rest_api_id, region_name in rest_apis:
×
2292
        with contextlib.suppress(Exception):
×
2293
            apigateway_client = aws_client_factory(region_name=region_name).apigateway
×
2294
            apigateway_client.delete_rest_api(restApiId=rest_api_id)
×
2295

2296

2297
@pytest.fixture
1✔
2298
def assert_host_customisation(monkeypatch):
1✔
2299
    localstack_host = "foo.bar"
1✔
2300
    monkeypatch.setattr(
1✔
2301
        config, "LOCALSTACK_HOST", config.HostAndPort(host=localstack_host, port=8888)
2302
    )
2303

2304
    def asserter(
1✔
2305
        url: str,
2306
        *,
2307
        custom_host: str | None = None,
2308
    ):
2309
        if custom_host is not None:
1✔
2310
            assert custom_host in url, f"Could not find `{custom_host}` in `{url}`"
×
2311

2312
            assert localstack_host not in url
×
2313
        else:
2314
            assert localstack_host in url, f"Could not find `{localstack_host}` in `{url}`"
1✔
2315

2316
    yield asserter
1✔
2317

2318

2319
@pytest.fixture
1✔
2320
def echo_http_server(httpserver: HTTPServer):
1✔
2321
    """Spins up a local HTTP echo server and returns the endpoint URL"""
2322

2323
    def _echo(request: Request) -> Response:
1✔
2324
        request_json = None
1✔
2325
        if request.is_json:
1✔
2326
            with contextlib.suppress(ValueError):
1✔
2327
                request_json = json.loads(request.data)
1✔
2328
        result = {
1✔
2329
            "data": request.data or "{}",
2330
            "headers": dict(request.headers),
2331
            "url": request.url,
2332
            "method": request.method,
2333
            "json": request_json,
2334
        }
2335
        response_body = json.dumps(json_safe(result))
1✔
2336
        return Response(response_body, status=200)
1✔
2337

2338
    httpserver.expect_request("").respond_with_handler(_echo)
1✔
2339
    http_endpoint = httpserver.url_for("/")
1✔
2340

2341
    return http_endpoint
1✔
2342

2343

2344
@pytest.fixture
1✔
2345
def echo_http_server_post(echo_http_server):
1✔
2346
    """
2347
    Returns an HTTP echo server URL for POST requests that work both locally and for parity tests (against real AWS)
2348
    """
2349
    if is_aws_cloud():
1✔
2350
        return f"{PUBLIC_HTTP_ECHO_SERVER_URL}/post"
×
2351

2352
    return f"{echo_http_server}post"
1✔
2353

2354

2355
def create_policy_doc(effect: str, actions: list, resource=None) -> dict:
1✔
2356
    actions = ensure_list(actions)
1✔
2357
    resource = resource or "*"
1✔
2358
    return {
1✔
2359
        "Version": "2012-10-17",
2360
        "Statement": [
2361
            {
2362
                # TODO statement ids have to be alphanumeric [0-9A-Za-z], write a test for it
2363
                "Sid": f"s{short_uid()}",
2364
                "Effect": effect,
2365
                "Action": actions,
2366
                "Resource": resource,
2367
            }
2368
        ],
2369
    }
2370

2371

2372
@pytest.fixture
1✔
2373
def create_policy_generated_document(create_policy):
1✔
2374
    def _create_policy_with_doc(effect, actions, policy_name=None, resource=None, iam_client=None):
1✔
2375
        policy_name = policy_name or f"p-{short_uid()}"
1✔
2376
        policy = create_policy_doc(effect, actions, resource=resource)
1✔
2377
        response = create_policy(
1✔
2378
            PolicyName=policy_name, PolicyDocument=json.dumps(policy), iam_client=iam_client
2379
        )
2380
        policy_arn = response["Policy"]["Arn"]
1✔
2381
        return policy_arn
1✔
2382

2383
    return _create_policy_with_doc
1✔
2384

2385

2386
@pytest.fixture
1✔
2387
def create_role_with_policy(create_role, create_policy_generated_document, aws_client):
1✔
2388
    def _create_role_with_policy(
1✔
2389
        effect, actions, assume_policy_doc, resource=None, attach=True, iam_client=None
2390
    ):
2391
        iam_client = iam_client or aws_client.iam
1✔
2392

2393
        role_name = f"role-{short_uid()}"
1✔
2394
        result = create_role(
1✔
2395
            RoleName=role_name, AssumeRolePolicyDocument=assume_policy_doc, iam_client=iam_client
2396
        )
2397
        role_arn = result["Role"]["Arn"]
1✔
2398
        policy_name = f"p-{short_uid()}"
1✔
2399

2400
        if attach:
1✔
2401
            # create role and attach role policy
2402
            policy_arn = create_policy_generated_document(
1✔
2403
                effect, actions, policy_name=policy_name, resource=resource, iam_client=iam_client
2404
            )
2405
            iam_client.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
1✔
2406
        else:
2407
            # put role policy
2408
            policy_document = create_policy_doc(effect, actions, resource=resource)
1✔
2409
            policy_document = json.dumps(policy_document)
1✔
2410
            iam_client.put_role_policy(
1✔
2411
                RoleName=role_name, PolicyName=policy_name, PolicyDocument=policy_document
2412
            )
2413

2414
        return role_name, role_arn
1✔
2415

2416
    return _create_role_with_policy
1✔
2417

2418

2419
@pytest.fixture
1✔
2420
def create_user_with_policy(create_policy_generated_document, create_user, aws_client, region_name):
1✔
2421
    def _create_user_with_policy(effect, actions, resource=None, user_name=None):
×
2422
        policy_arn = create_policy_generated_document(effect, actions, resource=resource)
×
2423
        username = user_name or f"user-{short_uid()}"
×
2424
        create_user(UserName=username)
×
2425
        aws_client.iam.attach_user_policy(UserName=username, PolicyArn=policy_arn)
×
2426
        keys = aws_client.iam.create_access_key(UserName=username)["AccessKey"]
×
2427

2428
        wait_for_user(keys=keys, region_name=region_name)
×
2429
        return username, keys
×
2430

2431
    return _create_user_with_policy
×
2432

2433

2434
@pytest.fixture()
1✔
2435
def register_extension(s3_bucket, aws_client):
1✔
2436
    cfn_client = aws_client.cloudformation
×
2437
    extensions_arns = []
×
2438

2439
    def _register(extension_name, extension_type, artifact_path):
×
2440
        bucket = s3_bucket
×
2441
        key = f"artifact-{short_uid()}"
×
2442

2443
        aws_client.s3.upload_file(artifact_path, bucket, key)
×
2444

2445
        register_response = cfn_client.register_type(
×
2446
            Type=extension_type,
2447
            TypeName=extension_name,
2448
            SchemaHandlerPackage=f"s3://{bucket}/{key}",
2449
        )
2450

2451
        registration_token = register_response["RegistrationToken"]
×
2452
        cfn_client.get_waiter("type_registration_complete").wait(
×
2453
            RegistrationToken=registration_token
2454
        )
2455

2456
        describe_response = cfn_client.describe_type_registration(
×
2457
            RegistrationToken=registration_token
2458
        )
2459

2460
        extensions_arns.append(describe_response["TypeArn"])
×
2461
        cfn_client.set_type_default_version(Arn=describe_response["TypeVersionArn"])
×
2462

2463
        return describe_response
×
2464

2465
    yield _register
×
2466

2467
    for arn in extensions_arns:
×
2468
        versions = cfn_client.list_type_versions(Arn=arn)["TypeVersionSummaries"]
×
2469
        for v in versions:
×
2470
            try:
×
2471
                cfn_client.deregister_type(Arn=v["Arn"])
×
2472
            except Exception:
×
2473
                continue
×
2474
        cfn_client.deregister_type(Arn=arn)
×
2475

2476

2477
@pytest.fixture
1✔
2478
def hosted_zone(aws_client):
1✔
2479
    zone_ids = []
1✔
2480

2481
    def factory(**kwargs):
1✔
2482
        if "CallerReference" not in kwargs:
1✔
2483
            kwargs["CallerReference"] = f"ref-{short_uid()}"
1✔
2484
        response = aws_client.route53.create_hosted_zone(**kwargs)
1✔
2485
        zone_id = response["HostedZone"]["Id"]
1✔
2486
        zone_ids.append(zone_id)
1✔
2487
        return response
1✔
2488

2489
    yield factory
1✔
2490

2491
    for zone_id in zone_ids[::-1]:
1✔
2492
        try:
1✔
2493
            aws_client.route53.delete_hosted_zone(Id=zone_id)
1✔
2494
        except ClientError as e:
1✔
2495
            LOG.debug("failed to delete hosted zone %s: %s", zone_id, e)
1✔
2496

2497

2498
@pytest.fixture
1✔
2499
def openapi_validate(monkeypatch):
1✔
2500
    monkeypatch.setattr(config, "OPENAPI_VALIDATE_RESPONSE", "true")
1✔
2501
    monkeypatch.setattr(config, "OPENAPI_VALIDATE_REQUEST", "true")
1✔
2502

2503

2504
@pytest.fixture
1✔
2505
def set_resource_custom_id():
1✔
2506
    set_ids = []
1✔
2507

2508
    def _set_custom_id(resource_identifier: ResourceIdentifier, custom_id):
1✔
2509
        localstack_id_manager.set_custom_id(
1✔
2510
            resource_identifier=resource_identifier, custom_id=custom_id
2511
        )
2512
        set_ids.append(resource_identifier)
1✔
2513

2514
    yield _set_custom_id
1✔
2515

2516
    for resource_identifier in set_ids:
1✔
2517
        localstack_id_manager.unset_custom_id(resource_identifier)
1✔
2518

2519

2520
###############################
2521
# Events (EventBridge) fixtures
2522
###############################
2523

2524

2525
@pytest.fixture
1✔
2526
def events_create_event_bus(aws_client, region_name, account_id):
1✔
2527
    event_bus_names = []
1✔
2528

2529
    def _create_event_bus(**kwargs):
1✔
2530
        if "Name" not in kwargs:
1✔
UNCOV
2531
            kwargs["Name"] = f"test-event-bus-{short_uid()}"
×
2532

2533
        response = aws_client.events.create_event_bus(**kwargs)
1✔
2534
        event_bus_names.append(kwargs["Name"])
1✔
2535
        return response
1✔
2536

2537
    yield _create_event_bus
1✔
2538

2539
    for event_bus_name in event_bus_names:
1✔
2540
        try:
1✔
2541
            response = aws_client.events.list_rules(EventBusName=event_bus_name)
1✔
2542
            rules = [rule["Name"] for rule in response["Rules"]]
1✔
2543

2544
            # Delete all rules for the current event bus
2545
            for rule in rules:
1✔
2546
                try:
1✔
2547
                    response = aws_client.events.list_targets_by_rule(
1✔
2548
                        Rule=rule, EventBusName=event_bus_name
2549
                    )
2550
                    targets = [target["Id"] for target in response["Targets"]]
1✔
2551

2552
                    # Remove all targets for the current rule
2553
                    if targets:
1✔
2554
                        for target in targets:
1✔
2555
                            aws_client.events.remove_targets(
1✔
2556
                                Rule=rule, EventBusName=event_bus_name, Ids=[target]
2557
                            )
2558

2559
                    aws_client.events.delete_rule(Name=rule, EventBusName=event_bus_name)
1✔
UNCOV
2560
                except Exception as e:
×
UNCOV
2561
                    LOG.warning("Failed to delete rule %s: %s", rule, e)
×
2562

2563
            # Delete archives for event bus
2564
            event_source_arn = (
1✔
2565
                f"arn:aws:events:{region_name}:{account_id}:event-bus/{event_bus_name}"
2566
            )
2567
            response = aws_client.events.list_archives(EventSourceArn=event_source_arn)
1✔
2568
            archives = [archive["ArchiveName"] for archive in response["Archives"]]
1✔
2569
            for archive in archives:
1✔
2570
                try:
1✔
2571
                    aws_client.events.delete_archive(ArchiveName=archive)
1✔
UNCOV
2572
                except Exception as e:
×
UNCOV
2573
                    LOG.warning("Failed to delete archive %s: %s", archive, e)
×
2574

2575
            aws_client.events.delete_event_bus(Name=event_bus_name)
1✔
2576
        except Exception as e:
1✔
2577
            LOG.warning("Failed to delete event bus %s: %s", event_bus_name, e)
1✔
2578

2579

2580
@pytest.fixture
1✔
2581
def events_put_rule(aws_client):
1✔
2582
    rules = []
1✔
2583

2584
    def _put_rule(**kwargs):
1✔
2585
        if "Name" not in kwargs:
1✔
UNCOV
2586
            kwargs["Name"] = f"rule-{short_uid()}"
×
2587

2588
        response = aws_client.events.put_rule(**kwargs)
1✔
2589
        rules.append((kwargs["Name"], kwargs.get("EventBusName", "default")))
1✔
2590
        return response
1✔
2591

2592
    yield _put_rule
1✔
2593

2594
    for rule, event_bus_name in rules:
1✔
2595
        try:
1✔
2596
            response = aws_client.events.list_targets_by_rule(
1✔
2597
                Rule=rule, EventBusName=event_bus_name
2598
            )
2599
            targets = [target["Id"] for target in response["Targets"]]
1✔
2600

2601
            # Remove all targets for the current rule
2602
            if targets:
1✔
2603
                for target in targets:
1✔
2604
                    aws_client.events.remove_targets(
1✔
2605
                        Rule=rule, EventBusName=event_bus_name, Ids=[target]
2606
                    )
2607

2608
            aws_client.events.delete_rule(Name=rule, EventBusName=event_bus_name)
1✔
2609
        except Exception as e:
1✔
2610
            LOG.warning("Failed to delete rule %s: %s", rule, e)
1✔
2611

2612

2613
@pytest.fixture
1✔
2614
def events_create_rule(aws_client):
1✔
2615
    rules = []
1✔
2616

2617
    def _create_rule(**kwargs):
1✔
2618
        rule_name = kwargs["Name"]
1✔
2619
        bus_name = kwargs.get("EventBusName", "")
1✔
2620
        pattern = kwargs.get("EventPattern", {})
1✔
2621
        schedule = kwargs.get("ScheduleExpression", "")
1✔
2622
        rule_arn = aws_client.events.put_rule(
1✔
2623
            Name=rule_name,
2624
            EventBusName=bus_name,
2625
            EventPattern=json.dumps(pattern),
2626
            ScheduleExpression=schedule,
2627
        )["RuleArn"]
2628
        rules.append({"name": rule_name, "bus": bus_name})
1✔
2629
        return rule_arn
1✔
2630

2631
    yield _create_rule
1✔
2632

2633
    for rule in rules:
1✔
2634
        targets = aws_client.events.list_targets_by_rule(
1✔
2635
            Rule=rule["name"], EventBusName=rule["bus"]
2636
        )["Targets"]
2637

2638
        targetIds = [target["Id"] for target in targets]
1✔
2639
        if len(targetIds) > 0:
1✔
2640
            aws_client.events.remove_targets(
1✔
2641
                Rule=rule["name"], EventBusName=rule["bus"], Ids=targetIds
2642
            )
2643

2644
        aws_client.events.delete_rule(Name=rule["name"], EventBusName=rule["bus"])
1✔
2645

2646

2647
@pytest.fixture
1✔
2648
def sqs_as_events_target(aws_client, sqs_get_queue_arn):
1✔
2649
    """
2650
    Fixture that creates an SQS queue and sets it up as a target for EventBridge events.
2651
    """
2652
    queue_urls = []
1✔
2653

2654
    def _sqs_as_events_target(
1✔
2655
        queue_name: str | None = None, custom_aws_client=None
2656
    ) -> tuple[str, str]:
2657
        if not queue_name:
1✔
2658
            queue_name = f"tests-queue-{short_uid()}"
1✔
2659
        if custom_aws_client:
1✔
UNCOV
2660
            sqs_client = custom_aws_client.sqs
×
2661
        else:
2662
            sqs_client = aws_client.sqs
1✔
2663
        queue_url = sqs_client.create_queue(QueueName=queue_name)["QueueUrl"]
1✔
2664
        queue_urls.append((queue_url, sqs_client))
1✔
2665
        queue_arn = sqs_get_queue_arn(queue_url)
1✔
2666
        policy = {
1✔
2667
            "Version": "2012-10-17",
2668
            "Id": f"sqs-eventbridge-{short_uid()}",
2669
            "Statement": [
2670
                {
2671
                    "Sid": f"SendMessage-{short_uid()}",
2672
                    "Effect": "Allow",
2673
                    "Principal": {"Service": "events.amazonaws.com"},
2674
                    "Action": "sqs:SendMessage",
2675
                    "Resource": queue_arn,
2676
                }
2677
            ],
2678
        }
2679
        sqs_client.set_queue_attributes(
1✔
2680
            QueueUrl=queue_url, Attributes={"Policy": json.dumps(policy)}
2681
        )
2682
        return queue_url, queue_arn, queue_name
1✔
2683

2684
    yield _sqs_as_events_target
1✔
2685

2686
    for queue_url, sqs_client in queue_urls:
1✔
2687
        try:
1✔
2688
            sqs_client.delete_queue(QueueUrl=queue_url)
1✔
UNCOV
2689
        except Exception as e:
×
UNCOV
2690
            LOG.debug("error cleaning up queue %s: %s", queue_url, e)
×
2691

2692

2693
@pytest.fixture
1✔
2694
def create_role_event_bus_source_to_bus_target(create_iam_role_with_policy):
1✔
2695
    def _create_role_event_bus_to_bus():
1✔
2696
        assume_role_policy_document_bus_source_to_bus_target = {
1✔
2697
            "Version": "2012-10-17",
2698
            "Statement": [
2699
                {
2700
                    "Effect": "Allow",
2701
                    "Principal": {"Service": "events.amazonaws.com"},
2702
                    "Action": "sts:AssumeRole",
2703
                }
2704
            ],
2705
        }
2706

2707
        policy_document_bus_source_to_bus_target = {
1✔
2708
            "Version": "2012-10-17",
2709
            "Statement": [
2710
                {
2711
                    "Sid": "",
2712
                    "Effect": "Allow",
2713
                    "Action": "events:PutEvents",
2714
                    "Resource": "arn:aws:events:*:*:event-bus/*",
2715
                }
2716
            ],
2717
        }
2718

2719
        role_arn_bus_source_to_bus_target = create_iam_role_with_policy(
1✔
2720
            RoleDefinition=assume_role_policy_document_bus_source_to_bus_target,
2721
            PolicyDefinition=policy_document_bus_source_to_bus_target,
2722
        )
2723

2724
        return role_arn_bus_source_to_bus_target
1✔
2725

2726
    yield _create_role_event_bus_to_bus
1✔
2727

2728

2729
@pytest.fixture
1✔
2730
def get_primary_secondary_client(
1✔
2731
    aws_client_factory,
2732
    secondary_aws_client_factory,
2733
    region_name,
2734
    secondary_region_name,
2735
    account_id,
2736
    secondary_account_id,
2737
):
2738
    def _get_primary_secondary_clients(cross_scenario: str):
1✔
2739
        """
2740
        Returns primary and secondary AWS clients based on the cross-scenario.
2741
        :param cross_scenario: The scenario for cross-region or cross-account testing.
2742
                               Options: "region", "account", "region_account"
2743
                               account_region cross scenario is not supported by AWS
2744
        :return: A dictionary containing primary and secondary AWS clients, and their respective region and account IDs.
2745
        """
2746
        secondary_region = secondary_region_name
1✔
2747
        secondary_account = secondary_account_id
1✔
2748
        if cross_scenario not in ["region", "account", "region_account"]:
1✔
UNCOV
2749
            raise ValueError(f"cross_scenario {cross_scenario} not supported")
×
2750

2751
        primary_client = aws_client_factory(region_name=region_name)
1✔
2752

2753
        if cross_scenario == "region":
1✔
2754
            secondary_account = account_id
1✔
2755
            secondary_client = aws_client_factory(region_name=secondary_region_name)
1✔
2756

2757
        elif cross_scenario == "account":
1✔
2758
            secondary_region = region_name
1✔
2759
            secondary_client = secondary_aws_client_factory(region_name=region_name)
1✔
2760

2761
        elif cross_scenario == "region_account":
1✔
2762
            secondary_client = secondary_aws_client_factory(region_name=secondary_region)
1✔
2763

2764
        return {
1✔
2765
            "primary_aws_client": primary_client,
2766
            "secondary_aws_client": secondary_client,
2767
            "secondary_region_name": secondary_region,
2768
            "secondary_account_id": secondary_account,
2769
        }
2770

2771
    return _get_primary_secondary_clients
1✔
2772

2773

2774
@pytest.fixture
1✔
2775
def clean_up(
1✔
2776
    aws_client,
2777
):  # TODO: legacy clean up fixtures for eventbridge - remove and use individual fixtures for creating resources instead
2778
    def _clean_up(
1✔
2779
        bus_name=None,
2780
        rule_name=None,
2781
        target_ids=None,
2782
        queue_url=None,
2783
        log_group_name=None,
2784
    ):
2785
        events_client = aws_client.events
1✔
2786
        kwargs = {"EventBusName": bus_name} if bus_name else {}
1✔
2787
        if target_ids:
1✔
2788
            target_ids = target_ids if isinstance(target_ids, list) else [target_ids]
1✔
2789
            call_safe(
1✔
2790
                events_client.remove_targets,
2791
                kwargs=dict(Rule=rule_name, Ids=target_ids, Force=True, **kwargs),
2792
            )
2793
        if rule_name:
1✔
2794
            call_safe(events_client.delete_rule, kwargs=dict(Name=rule_name, Force=True, **kwargs))
1✔
2795
        if bus_name:
1✔
2796
            call_safe(events_client.delete_event_bus, kwargs={"Name": bus_name})
×
2797
        if queue_url:
1✔
2798
            sqs_client = aws_client.sqs
×
UNCOV
2799
            call_safe(sqs_client.delete_queue, kwargs={"QueueUrl": queue_url})
×
2800
        if log_group_name:
1✔
2801
            logs_client = aws_client.logs
×
2802

2803
            def _delete_log_group():
×
UNCOV
2804
                log_streams = logs_client.describe_log_streams(logGroupName=log_group_name)
×
UNCOV
2805
                for log_stream in log_streams["logStreams"]:
×
2806
                    logs_client.delete_log_stream(
×
2807
                        logGroupName=log_group_name, logStreamName=log_stream["logStreamName"]
2808
                    )
UNCOV
2809
                logs_client.delete_log_group(logGroupName=log_group_name)
×
2810

UNCOV
2811
            call_safe(_delete_log_group)
×
2812

2813
    yield _clean_up
1✔
2814

2815

2816
@pytest.fixture
1✔
2817
def aws_catalog_mock(monkeypatch):
1✔
2818
    def _mock_catalog(path):
1✔
2819
        catalog = MagicMock()
1✔
2820
        monkeypatch.setattr(path, lambda: catalog)
1✔
2821
        return catalog
1✔
2822

2823
    return _mock_catalog
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc