Skip to content

Added test and fix for wrong time format and serialization in events v2 #11959

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion localstack-core/localstack/aws/api/events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,7 +919,7 @@ class EventSource(TypedDict, total=False):


EventSourceList = List[EventSource]
EventTime = datetime
EventTime = datetime | str
HeaderParametersMap = Dict[HeaderKey, HeaderValue]
QueryStringParametersMap = Dict[QueryStringKey, QueryStringValue]
PathParameterList = List[PathParameter]
Expand Down
5 changes: 4 additions & 1 deletion localstack-core/localstack/services/events/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,17 @@ def format_event(
message_id = message.get("original_id", str(long_uid()))
region = message.get("original_region", region)
account_id = message.get("original_account", account_id)
# Format the datetime to ISO-8601 string
event_time = get_event_time(event)
formatted_time = event_time_to_time_string(event_time)

formatted_event = {
"version": "0",
"id": message_id,
"detail-type": event.get("DetailType"),
"source": event.get("Source"),
"account": account_id,
"time": get_event_time(event),
"time": formatted_time,
"region": region,
"resources": event.get("Resources", []),
"detail": json.loads(event.get("Detail", "{}")),
Expand Down
64 changes: 64 additions & 0 deletions tests/aws/services/events/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
"""

import base64
import datetime
import json
import os
import re
import time
import uuid

Expand Down Expand Up @@ -578,6 +580,68 @@ def test_put_events_with_target_delivery_failure(

assert len(messages) == 0, "No messages should be delivered when queue doesn't exist"

@markers.aws.validated
@pytest.mark.skipif(is_old_provider(), reason="Test specific for v2 provider")
def test_put_events_with_time_field(
self, events_put_rule, create_sqs_events_target, aws_client, snapshot
):
"""Test that EventBridge correctly handles datetime serialization in events."""
rule_name = f"test-rule-{short_uid()}"
queue_url, queue_arn = create_sqs_events_target()

snapshot.add_transformers_list(
[
snapshot.transform.key_value("MD5OfBody", reference_replacement=False),
*snapshot.transform.sqs_api(),
]
)

events_put_rule(
Name=rule_name,
EventPattern=json.dumps(
{"source": ["test-source"], "detail-type": ["test-detail-type"]}
),
)

aws_client.events.put_targets(Rule=rule_name, Targets=[{"Id": "id1", "Arn": queue_arn}])

timestamp = datetime.datetime.utcnow()
event = {
"Source": "test-source",
"DetailType": "test-detail-type",
"Time": timestamp,
"Detail": json.dumps({"message": "test message"}),
}

response = aws_client.events.put_events(Entries=[event])
snapshot.match("put-events", response)

messages = sqs_collect_messages(aws_client, queue_url, expected_events_count=1)
assert len(messages) == 1
snapshot.match("sqs-messages", messages)

received_event = json.loads(messages[0]["Body"])
# Explicit assertions for time field format GH issue: https://github.com/localstack/localstack/issues/11630#issuecomment-2506187279
assert "time" in received_event, "Time field missing in the event"
time_str = received_event["time"]

# Verify ISO8601 format: YYYY-MM-DDThh:mm:ssZ
# Example: "2024-11-28T13:44:36Z"
assert re.match(
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$", time_str
), f"Time field '{time_str}' does not match ISO8601 format (YYYY-MM-DDThh:mm:ssZ)"

# Verify we can parse it back to datetime
datetime_obj = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%SZ")
assert isinstance(
datetime_obj, datetime.datetime
), f"Failed to parse time string '{time_str}' back to datetime object"

time_difference = abs((datetime_obj - timestamp.replace(microsecond=0)).total_seconds())
assert (
time_difference <= 60
), f"Time in event '{time_str}' differs too much from sent time '{timestamp.isoformat()}'"


class TestEventBus:
@markers.aws.validated
Expand Down
37 changes: 37 additions & 0 deletions tests/aws/services/events/test_events.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -2695,5 +2695,42 @@
}
}
}
},
"tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_time_field": {
"recorded-date": "28-11-2024, 21:25:00",
"recorded-content": {
"put-events": {
"Entries": [
{
"EventId": "<uuid:1>"
}
],
"FailedEntryCount": 0,
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"sqs-messages": [
{
"MessageId": "<uuid:2>",
"ReceiptHandle": "<receipt-handle:1>",
"MD5OfBody": "m-d5-of-body",
"Body": {
"version": "0",
"id": "<uuid:1>",
"detail-type": "test-detail-type",
"source": "test-source",
"account": "111111111111",
"time": "date",
"region": "<region>",
"resources": [],
"detail": {
"message": "test message"
}
}
}
]
}
}
}
3 changes: 3 additions & 0 deletions tests/aws/services/events/test_events.validation.json
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@
"tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_target_delivery_failure": {
"last_validated_date": "2024-11-20T17:19:19+00:00"
},
"tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_time_field": {
"last_validated_date": "2024-11-28T21:25:00+00:00"
},
"tests/aws/services/events/test_events.py::TestEvents::test_put_events_without_source": {
"last_validated_date": "2024-06-19T10:40:50+00:00"
}
Expand Down
Loading