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

localstack / localstack / 22709357475

05 Mar 2026 08:35AM UTC coverage: 59.732% (-27.2%) from 86.974%
22709357475

Pull #13880

github

web-flow
Merge 28fcab93c into 710618057
Pull Request #13880: Firehose: Replace TaggingService

12 of 12 new or added lines in 2 files covered. (100.0%)

20464 existing lines in 510 files now uncovered.

45290 of 75822 relevant lines covered (59.73%)

0.6 hits per line

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

52.61
/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
    """
UNCOV
102
    no_retry_config = botocore.config.Config(retries={"max_attempts": 1})
×
UNCOV
103
    return aws_client_factory(config=no_retry_config)
×
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:
UNCOV
141
            credentials = aws_session.get_credentials()
×
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✔
UNCOV
160
    return aws_client_factory(
×
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✔
UNCOV
167
    def wait_for_table_active(table_name: str, client=None):
×
UNCOV
168
        def wait():
×
UNCOV
169
            return (client or aws_client.dynamodb).describe_table(TableName=table_name)["Table"][
×
170
                "TableStatus"
171
            ] == "ACTIVE"
172

UNCOV
173
        poll_condition(wait, timeout=30)
×
174

UNCOV
175
    return wait_for_table_active
×
176

177

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

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

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

UNCOV
191
    yield factory
×
192

193
    # cleanup
UNCOV
194
    for table in tables:
×
UNCOV
195
        try:
×
196
            # table has to be in ACTIVE state before deletion
UNCOV
197
            dynamodb_wait_for_table_active(table)
×
UNCOV
198
            aws_client.dynamodb.delete_table(TableName=table)
×
UNCOV
199
        except Exception as e:
×
UNCOV
200
            LOG.debug("error cleaning up table %s: %s", table, e)
×
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
UNCOV
206
    tables = []
×
207

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

UNCOV
215
        tables.append(kwargs["table_name"])
×
216

UNCOV
217
        return create_dynamodb_table(**kwargs)
×
218

UNCOV
219
    yield factory
×
220

221
    # cleanup
UNCOV
222
    for table in tables:
×
UNCOV
223
        try:
×
224
            # table has to be in ACTIVE state before deletion
UNCOV
225
            dynamodb_wait_for_table_active(table)
×
UNCOV
226
            aws_client.dynamodb.delete_table(TableName=table)
×
UNCOV
227
        except Exception as e:
×
UNCOV
228
            LOG.debug("error cleaning up table %s: %s", table, e)
×
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✔
UNCOV
264
    buckets = []
×
265

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

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

UNCOV
274
    yield factory
×
275

276
    # cleanup
UNCOV
277
    for bucket in buckets:
×
UNCOV
278
        try:
×
UNCOV
279
            s3_empty_bucket(bucket)
×
UNCOV
280
            aws_client.s3.delete_bucket(Bucket=bucket)
×
UNCOV
281
        except Exception as e:
×
UNCOV
282
            LOG.debug("error cleaning up bucket %s: %s", bucket, e)
×
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✔
UNCOV
306
            kwargs["BypassGovernanceRetention"] = True
×
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✔
UNCOV
325
            aws_client.s3.delete_objects(
×
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✔
UNCOV
470
    return sqs_create_queue()
×
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✔
UNCOV
524
    def wait_for_topic_delete(topic_arn: str) -> None:
×
UNCOV
525
        def wait():
×
UNCOV
526
            try:
×
UNCOV
527
                aws_client.sns.get_topic_attributes(TopicArn=topic_arn)
×
528
                return False
×
UNCOV
529
            except Exception as e:
×
UNCOV
530
                if "NotFound" in e.response["Error"]["Code"]:
×
UNCOV
531
                    return True
×
532

533
                raise
×
534

UNCOV
535
        poll_condition(wait, timeout=30)
×
536

UNCOV
537
    return wait_for_topic_delete
×
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✔
UNCOV
685
    hosted_zones = []
×
686

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

UNCOV
698
    yield factory
×
699

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

706

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

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

UNCOV
714
        if not params:
×
UNCOV
715
            params = {}
×
716

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

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

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

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

UNCOV
730
        response = aws_client.transcribe.start_transcription_job(**params)
×
731

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

UNCOV
735
        return job_name
×
736

UNCOV
737
    yield _create_job
×
738

UNCOV
739
    for job_name in job_names:
×
UNCOV
740
        with contextlib.suppress(ClientError):
×
UNCOV
741
            aws_client.transcribe.delete_transcription_job(TranscriptionJobName=job_name)
×
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✔
UNCOV
760
        except Exception as e:
×
UNCOV
761
            LOG.debug("error cleaning up kinesis stream %s: %s", stream_name, e)
×
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✔
UNCOV
798
    def _wait_for_stream_ready(stream_arn: str, client=None):
×
UNCOV
799
        def is_stream_ready():
×
UNCOV
800
            ddb_client = client or aws_client.dynamodbstreams
×
UNCOV
801
            describe_stream_response = ddb_client.describe_stream(StreamArn=stream_arn)
×
UNCOV
802
            return describe_stream_response["StreamDescription"]["StreamStatus"] == "ENABLED"
×
803

UNCOV
804
        return poll_condition(is_stream_ready)
×
805

UNCOV
806
    return _wait_for_stream_ready
×
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
            )
UNCOV
830
        except Exception as e:
×
UNCOV
831
            exception_message = str(e)
×
832
            # Some tests schedule their keys for deletion themselves.
UNCOV
833
            if (
×
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✔
UNCOV
842
    key_ids = []
×
843

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

UNCOV
849
    yield _replicate_key
×
850

UNCOV
851
    for region_to, key_id in key_ids:
×
UNCOV
852
        try:
×
853
            # shortest amount of time you can schedule the deletion
UNCOV
854
            aws_client_factory(region_name=region_to).kms.schedule_key_deletion(
×
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✔
UNCOV
866
    aliases = []
×
867

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

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

UNCOV
878
    yield _create_alias
×
879

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

886

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

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

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

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

UNCOV
909
    yield _create_grant
×
910

UNCOV
911
    for grant_id, key_id in grants:
×
UNCOV
912
        try:
×
UNCOV
913
            aws_client.kms.retire_grant(GrantId=grant_id, KeyId=key_id)
×
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✔
UNCOV
920
    return kms_create_key()
×
921

922

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

UNCOV
927
    return [
×
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✔
UNCOV
939
    def _wait_for_cluster(domain_name: str):
×
UNCOV
940
        def finished_processing():
×
UNCOV
941
            status = aws_client.opensearch.describe_domain(DomainName=domain_name)["DomainStatus"]
×
UNCOV
942
            return status["DomainProcessingStatus"] == "Active" and "Endpoint" in status
×
943

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

UNCOV
948
    return _wait_for_cluster
×
949

950

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

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

UNCOV
959
        aws_client.opensearch.create_domain(**kwargs)
×
960

UNCOV
961
        opensearch_wait_for_cluster(domain_name=kwargs["DomainName"])
×
962

UNCOV
963
        domains.append(kwargs["DomainName"])
×
UNCOV
964
        return kwargs["DomainName"]
×
965

UNCOV
966
    yield factory
×
967

968
    # cleanup
UNCOV
969
    for domain in domains:
×
UNCOV
970
        try:
×
UNCOV
971
            aws_client.opensearch.delete_domain(DomainName=domain)
×
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✔
UNCOV
978
    return opensearch_create_domain()
×
979

980

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

987

988
@pytest.fixture
1✔
989
def opensearch_document_path(opensearch_endpoint, aws_client):
1✔
UNCOV
990
    document = {
×
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
    }
UNCOV
997
    document_path = f"{opensearch_endpoint}/bountyhunters/_doc/1"
×
UNCOV
998
    response = requests.put(
×
999
        document_path,
1000
        data=json.dumps(document),
1001
        headers={"content-type": "application/json", "Accept-encoding": "identity"},
1002
    )
UNCOV
1003
    assert response.status_code == 201, f"could not create document at: {document_path}"
×
UNCOV
1004
    return document_path
×
1005

1006

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

UNCOV
1019
    return _cleanup_stacks
×
1020

1021

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

UNCOV
1032
    return _cleanup_changesets
×
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✔
UNCOV
1052
        self.describe_result = describe_res
×
UNCOV
1053
        self.events = events
×
1054

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

UNCOV
1061
        super().__init__(msg)
×
1062

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

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

UNCOV
1071
        return "\n".join(formatted_events)
×
1072

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

1081

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

UNCOV
1088
    def _deploy(
×
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:
UNCOV
1103
        if is_update:
×
UNCOV
1104
            assert stack_name
×
UNCOV
1105
        stack_name = stack_name or f"stack-{short_uid()}"
×
UNCOV
1106
        change_set_name = change_set_name or f"change-set-{short_uid()}"
×
1107

UNCOV
1108
        if max_wait is None:
×
UNCOV
1109
            max_wait = 1800 if is_aws_cloud() else 180
×
1110

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

UNCOV
1117
        kwargs = CreateChangeSetInput(
×
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
        )
UNCOV
1124
        kwargs["Parameters"] = []
×
UNCOV
1125
        if parameters:
×
UNCOV
1126
            kwargs["Parameters"] = [
×
1127
                Parameter(ParameterKey=k, ParameterValue=v) for (k, v) in parameters.items()
1128
            ]
UNCOV
1129
        elif raw_parameters:
×
1130
            kwargs["Parameters"] = raw_parameters
×
1131

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

UNCOV
1135
        cfn_aws_client = custom_aws_client if custom_aws_client is not None else aws_client
×
1136

UNCOV
1137
        response = cfn_aws_client.cloudformation.create_change_set(**kwargs)
×
1138

UNCOV
1139
        change_set_id = response["Id"]
×
UNCOV
1140
        stack_id = response["StackId"]
×
1141

UNCOV
1142
        try:
×
UNCOV
1143
            cfn_aws_client.cloudformation.get_waiter(WAITER_CHANGE_SET_CREATE_COMPLETE).wait(
×
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

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

UNCOV
1157
        try:
×
UNCOV
1158
            stack_waiter.wait(
×
1159
                StackName=stack_id,
1160
                WaiterConfig={
1161
                    "Delay": delay_between_polls,
1162
                    "MaxAttempts": max_wait / delay_between_polls,
1163
                },
1164
            )
UNCOV
1165
        except botocore.exceptions.WaiterError as e:
×
UNCOV
1166
            raise StackDeployError(
×
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

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

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

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

UNCOV
1190
        state.append((stack_id, _destroy_stack))
×
1191

UNCOV
1192
        return DeployResult(
×
1193
            change_set_id, stack_id, stack_name, change_set_name, mapped_outputs, _destroy_stack
1194
        )
1195

UNCOV
1196
    yield _deploy
×
1197

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

1205

1206
@pytest.fixture
1✔
1207
def is_change_set_created_and_available(aws_client):
1✔
UNCOV
1208
    def _is_change_set_created_and_available(change_set_id: str):
×
UNCOV
1209
        def _inner():
×
UNCOV
1210
            change_set = aws_client.cloudformation.describe_change_set(ChangeSetName=change_set_id)
×
UNCOV
1211
            return (
×
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

UNCOV
1217
        return _inner
×
1218

UNCOV
1219
    return _is_change_set_created_and_available
×
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✔
UNCOV
1240
    return _has_stack_status(aws_client.cloudformation, ["CREATE_COMPLETE", "CREATE_FAILED"])
×
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✔
UNCOV
1254
    def _has_status(stack_id: str):
×
UNCOV
1255
        def _inner():
×
UNCOV
1256
            resp = cfn_client.describe_stacks(StackName=stack_id)
×
UNCOV
1257
            s = resp["Stacks"][0]  # since the lookup  uses the id we can only get a single response
×
UNCOV
1258
            return s.get("StackStatus") in statuses
×
1259

UNCOV
1260
        return _inner
×
1261

UNCOV
1262
    return _has_status
×
1263

1264

1265
@pytest.fixture
1✔
1266
def is_change_set_finished(aws_client):
1✔
UNCOV
1267
    def _is_change_set_finished(change_set_id: str, stack_name: str | None = None):
×
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

UNCOV
1283
    return _is_change_set_finished
×
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✔
UNCOV
1348
    lambda_arns = []
×
1349

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

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

UNCOV
1368
            wait_until(_is_not_pending)
×
UNCOV
1369
            return resp
×
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
UNCOV
1373
        return retry(_create_function, retries=3, sleep=4)
×
1374

UNCOV
1375
    yield _create_lambda_function
×
1376

UNCOV
1377
    for arn in lambda_arns:
×
UNCOV
1378
        try:
×
UNCOV
1379
            aws_client.lambda_.delete_function(FunctionName=arn)
×
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✔
UNCOV
1460
    from localstack.aws.api.lambda_ import Runtime
×
1461

UNCOV
1462
    lambda_client = aws_client.lambda_
×
UNCOV
1463
    handler_code = textwrap.dedent(
×
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

UNCOV
1500
    def _create_echo_http_server(trim_x_headers: bool = False) -> str:
×
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
        }"""
UNCOV
1514
        zip_file = testutil.create_lambda_archive(handler_code, get_content=True)
×
UNCOV
1515
        func_name = f"echo-http-{short_uid()}"
×
UNCOV
1516
        create_lambda_function(
×
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
        )
UNCOV
1522
        url_response = lambda_client.create_function_url_config(
×
1523
            FunctionName=func_name, AuthType="NONE"
1524
        )
UNCOV
1525
        aws_client.lambda_.add_permission(
×
1526
            FunctionName=func_name,
1527
            StatementId="urlPermission",
1528
            Action="lambda:InvokeFunctionUrl",
1529
            Principal="*",
1530
            FunctionUrlAuthType="NONE",
1531
        )
UNCOV
1532
        return url_response["FunctionUrl"]
×
1533

UNCOV
1534
    yield _create_echo_http_server
×
1535

1536

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

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

UNCOV
1546
    yield _create_event_source_mapping
×
1547

UNCOV
1548
    for uuid in uuids:
×
UNCOV
1549
        try:
×
UNCOV
1550
            aws_client.lambda_.delete_event_source_mapping(UUID=uuid)
×
UNCOV
1551
        except aws_client.lambda_.exceptions.ResourceNotFoundException:
×
UNCOV
1552
            pass
×
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✔
UNCOV
1559
    def _check_logs(func_name: str, expected_lines: list[str] = None) -> list[str]:
×
UNCOV
1560
        if not expected_lines:
×
1561
            expected_lines = []
×
UNCOV
1562
        log_events = get_lambda_logs(func_name, logs_client=aws_client.logs)
×
UNCOV
1563
        log_messages = [e["message"] for e in log_events]
×
UNCOV
1564
        for line in expected_lines:
×
UNCOV
1565
            if ".*" in line:
×
UNCOV
1566
                found = [re.match(line, m, flags=re.DOTALL) for m in log_messages]
×
UNCOV
1567
                if any(found):
×
UNCOV
1568
                    continue
×
1569
            assert line in log_messages
×
UNCOV
1570
        return log_messages
×
1571

UNCOV
1572
    return _check_logs
×
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✔
UNCOV
1582
            kwargs["PolicyName"] = f"policy-{short_uid()}"
×
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✔
UNCOV
1593
        except Exception:
×
UNCOV
1594
            LOG.debug("Could not delete policy '%s' during test cleanup", policy_arn)
×
1595

1596

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

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

UNCOV
1608
    yield _create_user
×
1609

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

UNCOV
1619
        for inline_policy in inline_policies:
×
UNCOV
1620
            try:
×
UNCOV
1621
                aws_client.iam.delete_user_policy(UserName=username, PolicyName=inline_policy)
×
1622
            except Exception:
×
1623
                LOG.debug(
×
1624
                    "Could not delete user policy '%s' from '%s' during cleanup",
1625
                    inline_policy,
1626
                    username,
1627
                )
UNCOV
1628
        attached_policies = aws_client.iam.list_attached_user_policies(UserName=username)[
×
1629
            "AttachedPolicies"
1630
        ]
UNCOV
1631
        for attached_policy in attached_policies:
×
UNCOV
1632
            try:
×
UNCOV
1633
                aws_client.iam.detach_user_policy(
×
1634
                    UserName=username, PolicyArn=attached_policy["PolicyArn"]
1635
                )
UNCOV
1636
            except Exception:
×
UNCOV
1637
                LOG.debug(
×
1638
                    "Error detaching policy '%s' from user '%s'",
1639
                    attached_policy["PolicyArn"],
1640
                    username,
1641
                )
UNCOV
1642
        access_keys = aws_client.iam.list_access_keys(UserName=username)["AccessKeyMetadata"]
×
UNCOV
1643
        for access_key in access_keys:
×
UNCOV
1644
            try:
×
UNCOV
1645
                aws_client.iam.delete_access_key(
×
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

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

1660

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

UNCOV
1667
        def assume_role():
×
UNCOV
1668
            return aws_client.sts.assume_role(
×
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
UNCOV
1673
        keys = retry(assume_role, sleep=5, retries=4)
×
UNCOV
1674
        return keys
×
1675

UNCOV
1676
    return _wait_and_assume_role
×
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
            ]
UNCOV
1699
        except ClientError as e:
×
UNCOV
1700
            LOG.debug(
×
1701
                "Cannot list attached role policies: %s. Role %s probably already deleted...",
1702
                e,
1703
                role_name,
1704
            )
UNCOV
1705
            continue
×
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✔
UNCOV
1735
    params = []
×
1736

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

UNCOV
1741
    yield _create_parameter
×
1742

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

1746

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

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

UNCOV
1756
    yield _create_parameter
×
1757

UNCOV
1758
    for item in items:
×
UNCOV
1759
        aws_client.secretsmanager.delete_secret(SecretId=item, ForceDeleteWithoutRecovery=True)
×
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✔
UNCOV
1781
    certificate_arns = []
×
1782

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

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

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

UNCOV
1795
    yield factory
×
1796

1797
    # cleanup
UNCOV
1798
    for certificate_arn, region_name in certificate_arns:
×
UNCOV
1799
        try:
×
UNCOV
1800
            acm_client = aws_client_factory(region_name=region_name).acm
×
UNCOV
1801
            acm_client.delete_certificate(CertificateArn=certificate_arn)
×
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 = []
1✔
1842

1843
    def _inner(**kwargs: dict[str, any]) -> str:
1✔
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:
1✔
1851
            kwargs["RoleName"] = f"test-role-{short_uid()}"
×
1852

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

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

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

1862
        roles.append(role)
1✔
1863
        return role_arn
1✔
1864

1865
    yield _inner
1✔
1866

1867
    for role in roles:
1✔
1868
        try:
1✔
1869
            aws_client.iam.delete_role(RoleName=role)
1✔
1870
        except Exception as exc:
1✔
1871
            LOG.debug("Error deleting IAM role '%s': %s", role, exc)
1✔
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✔
UNCOV
1944
    configuration_set_names = []
×
1945

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

UNCOV
1954
    yield factory
×
1955

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

1959

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

UNCOV
1964
    def factory(config_set_name: str, event_destination_name: str, topic_arn: str) -> None:
×
UNCOV
1965
        aws_client.ses.create_configuration_set_event_destination(
×
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
        )
UNCOV
1976
        event_destinations.append((config_set_name, event_destination_name))
×
1977

UNCOV
1978
    yield factory
×
1979

UNCOV
1980
    for created_config_set_name, created_event_destination_name in event_destinations:
×
UNCOV
1981
        aws_client.ses.delete_configuration_set_event_destination(
×
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✔
UNCOV
1989
    template_names = []
×
1990

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

UNCOV
2001
    yield factory
×
2002

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

2006

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

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

UNCOV
2014
    yield factory
×
2015

UNCOV
2016
    for identity in identities:
×
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

UNCOV
2028
    def inner(sender_email_address: str | None = None) -> str:
×
UNCOV
2029
        if is_aws_cloud():
×
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
UNCOV
2036
            sender_email_address = f"sender-{short_uid()}@example.com"
×
UNCOV
2037
            ses_verify_identity(sender_email_address)
×
2038

UNCOV
2039
        return sender_email_address
×
2040

UNCOV
2041
    return inner
×
2042

2043

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

UNCOV
2048
    def factory(ports=None, ip_protocol: str = "tcp", **kwargs):
×
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
        """
UNCOV
2054
        if "GroupName" not in kwargs:
×
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
UNCOV
2059
        _args = select_from_typed_dict(CreateSecurityGroupRequest, kwargs)
×
UNCOV
2060
        security_group = aws_client.ec2.create_security_group(**_args)
×
UNCOV
2061
        security_group_id = security_group["GroupId"]
×
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.
UNCOV
2065
        permissions = [
×
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
        ]
UNCOV
2074
        aws_client.ec2.authorize_security_group_ingress(
×
2075
            GroupId=security_group_id,
2076
            IpPermissions=permissions,
2077
        )
2078

UNCOV
2079
        ec2_sgs.append(security_group_id)
×
UNCOV
2080
        return security_group
×
2081

UNCOV
2082
    yield factory
×
2083

UNCOV
2084
    for sg_group_id in ec2_sgs:
×
UNCOV
2085
        try:
×
UNCOV
2086
            aws_client.ec2.delete_security_group(GroupId=sg_group_id)
×
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✔
UNCOV
2093
    vpc_endpoints = []
×
2094

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

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

UNCOV
2105
            return _endpoint_details
×
2106

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

UNCOV
2109
    yield _create
×
2110

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

UNCOV
2116
    def wait_for_endpoint_deleted():
×
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
UNCOV
2126
    if is_aws_cloud():
×
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✔
UNCOV
2140
            http_code = e.response["ResponseMetadata"]["HTTPStatusCode"]
×
2141
            # Covers non-standardized error codes such as NotFoundException, NoSuchEntity (IAM), NoSuchBucket (S3), etc
UNCOV
2142
            if http_code == 404:
×
UNCOV
2143
                LOG.warning(
×
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:
UNCOV
2150
                LOG.warning(
×
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✔
UNCOV
2181
    return get_partition(region_name)
×
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✔
UNCOV
2259
            api_stages = item.get("apiStages", [])
×
UNCOV
2260
            usage_plan_ids.extend(
×
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✔
UNCOV
2270
            usage_plan_keys = apigateway_client.get_usage_plan_keys(usagePlanId=usage_plan_id)
×
UNCOV
2271
            for key in usage_plan_keys.get("items", []):
×
2272
                apigateway_client.delete_api_key(apiKey=key["id"])
×
UNCOV
2273
            apigateway_client.delete_usage_plan(usagePlanId=usage_plan_id)
×
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✔
UNCOV
2299
    localstack_host = "foo.bar"
×
UNCOV
2300
    monkeypatch.setattr(
×
2301
        config, "LOCALSTACK_HOST", config.HostAndPort(host=localstack_host, port=8888)
2302
    )
2303

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

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

UNCOV
2316
    yield asserter
×
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

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

UNCOV
2338
    httpserver.expect_request("").respond_with_handler(_echo)
×
UNCOV
2339
    http_endpoint = httpserver.url_for("/")
×
2340

UNCOV
2341
    return http_endpoint
×
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
    """
UNCOV
2349
    if is_aws_cloud():
×
2350
        return f"{PUBLIC_HTTP_ECHO_SERVER_URL}/post"
×
2351

UNCOV
2352
    return f"{echo_http_server}post"
×
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✔
UNCOV
2375
        policy_name = policy_name or f"p-{short_uid()}"
×
UNCOV
2376
        policy = create_policy_doc(effect, actions, resource=resource)
×
UNCOV
2377
        response = create_policy(
×
2378
            PolicyName=policy_name, PolicyDocument=json.dumps(policy), iam_client=iam_client
2379
        )
UNCOV
2380
        policy_arn = response["Policy"]["Arn"]
×
UNCOV
2381
        return policy_arn
×
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
UNCOV
2402
            policy_arn = create_policy_generated_document(
×
2403
                effect, actions, policy_name=policy_name, resource=resource, iam_client=iam_client
2404
            )
UNCOV
2405
            iam_client.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
×
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✔
UNCOV
2479
    zone_ids = []
×
2480

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

UNCOV
2489
    yield factory
×
2490

UNCOV
2491
    for zone_id in zone_ids[::-1]:
×
UNCOV
2492
        try:
×
UNCOV
2493
            aws_client.route53.delete_hosted_zone(Id=zone_id)
×
UNCOV
2494
        except ClientError as e:
×
UNCOV
2495
            LOG.debug("failed to delete hosted zone %s: %s", zone_id, e)
×
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✔
UNCOV
2506
    set_ids = []
×
2507

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

UNCOV
2514
    yield _set_custom_id
×
2515

UNCOV
2516
    for resource_identifier in set_ids:
×
UNCOV
2517
        localstack_id_manager.unset_custom_id(resource_identifier)
×
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✔
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✔
2560
                except Exception as e:
×
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✔
2572
                except Exception as e:
×
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✔
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✔
UNCOV
2615
    rules = []
×
2616

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

UNCOV
2631
    yield _create_rule
×
2632

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

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

UNCOV
2644
        aws_client.events.delete_rule(Name=rule["name"], EventBusName=rule["bus"])
×
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✔
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✔
2689
        except Exception as e:
×
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✔
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
×
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():
×
2804
                log_streams = logs_client.describe_log_streams(logGroupName=log_group_name)
×
2805
                for log_stream in log_streams["logStreams"]:
×
2806
                    logs_client.delete_log_stream(
×
2807
                        logGroupName=log_group_name, logStreamName=log_stream["logStreamName"]
2808
                    )
2809
                logs_client.delete_log_group(logGroupName=log_group_name)
×
2810

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✔
2824

2825

2826
@pytest.fixture
1✔
2827
def logs_log_group(aws_client):
1✔
2828
    """Create a log group for testing and clean up afterwards."""
2829

2830
    log_group_names = []
1✔
2831

2832
    def _create_log_group():
1✔
2833
        log_group_name = f"test-log-group-{short_uid()}"
1✔
2834
        aws_client.logs.create_log_group(logGroupName=log_group_name)
1✔
2835
        log_group_names.append(log_group_name)
1✔
2836
        return log_group_name
1✔
2837

2838
    yield _create_log_group()
1✔
2839

2840
    for group_name in log_group_names:
1✔
2841
        aws_client.logs.delete_log_group(logGroupName=group_name)
1✔
2842

2843

2844
@pytest.fixture
1✔
2845
def logs_log_stream(logs_log_group, aws_client):
1✔
2846
    """Create a log stream for testing and clean up afterwards."""
2847
    log_stream_names = []
1✔
2848

2849
    def _create_log_stream():
1✔
2850
        log_stream_name = f"test-log-stream-{short_uid()}"
1✔
2851
        aws_client.logs.create_log_stream(
1✔
2852
            logGroupName=logs_log_group, logStreamName=log_stream_name
2853
        )
2854
        log_stream_names.append(log_stream_name)
1✔
2855
        return log_stream_name
1✔
2856

2857
    yield _create_log_stream()
1✔
2858

2859
    for stream_name in log_stream_names:
1✔
2860
        aws_client.logs.delete_log_stream(logStreamName=stream_name, logGroupName=logs_log_group)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc