Skip to content

Commit 7b8fd7c

Browse files
authored
Merge pull request #207 from risenberg-cyberark/sns
feat: Add sns notification support to Parser utility #206
2 parents 805ba53 + b470a31 commit 7b8fd7c

File tree

9 files changed

+204
-8
lines changed

9 files changed

+204
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .base import BaseEnvelope
22
from .dynamodb import DynamoDBStreamEnvelope
33
from .event_bridge import EventBridgeEnvelope
4+
from .sns import SnsEnvelope
45
from .sqs import SqsEnvelope
56

6-
__all__ = ["DynamoDBStreamEnvelope", "EventBridgeEnvelope", "SqsEnvelope", "BaseEnvelope"]
7+
__all__ = ["DynamoDBStreamEnvelope", "EventBridgeEnvelope", "SnsEnvelope", "SqsEnvelope", "BaseEnvelope"]
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import logging
2+
from typing import Any, Dict, List, Optional, Union
3+
4+
from ..models import SnsModel
5+
from ..types import Model
6+
from .base import BaseEnvelope
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
class SnsEnvelope(BaseEnvelope):
12+
"""SNS Envelope to extract array of Records
13+
14+
The record's body parameter is a string, though it can also be a JSON encoded string.
15+
Regardless of its type it'll be parsed into a BaseModel object.
16+
17+
Note: Records will be parsed the same way so if model is str,
18+
all items in the list will be parsed as str and npt as JSON (and vice versa)
19+
"""
20+
21+
def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Model) -> List[Optional[Model]]:
22+
"""Parses records found with model provided
23+
24+
Parameters
25+
----------
26+
data : Dict
27+
Lambda event to be parsed
28+
model : Model
29+
Data model provided to parse after extracting data using envelope
30+
31+
Returns
32+
-------
33+
List
34+
List of records parsed with model provided
35+
"""
36+
logger.debug(f"Parsing incoming data with SNS model {SnsModel}")
37+
parsed_envelope = SnsModel.parse_obj(data)
38+
output = []
39+
logger.debug(f"Parsing SNS records in `body` with {model}")
40+
for record in parsed_envelope.Records:
41+
output.append(self._parse(data=record.Sns.Message, model=model))
42+
return output
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
from .dynamodb import DynamoDBStreamChangedRecordModel, DynamoDBStreamModel, DynamoDBStreamRecordModel
22
from .event_bridge import EventBridgeModel
3+
from .sns import SnsModel, SnsNotificationModel, SnsRecordModel
34
from .sqs import SqsModel, SqsRecordModel
45

56
__all__ = [
67
"DynamoDBStreamModel",
78
"EventBridgeModel",
89
"DynamoDBStreamChangedRecordModel",
910
"DynamoDBStreamRecordModel",
11+
"SnsModel",
12+
"SnsNotificationModel",
13+
"SnsRecordModel",
1014
"SqsModel",
1115
"SqsRecordModel",
1216
]

Diff for: aws_lambda_powertools/utilities/parser/models/sns.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from datetime import datetime
2+
from typing import Dict, List, Optional
3+
4+
from pydantic import BaseModel
5+
from pydantic.networks import HttpUrl
6+
from typing_extensions import Literal
7+
8+
9+
class SnsMsgAttributeModel(BaseModel):
10+
Type: str
11+
Value: str
12+
13+
14+
class SnsNotificationModel(BaseModel):
15+
Subject: Optional[str]
16+
TopicArn: str
17+
UnsubscribeUrl: HttpUrl
18+
Type: Literal["Notification"]
19+
MessageAttributes: Dict[str, SnsMsgAttributeModel]
20+
Message: str
21+
MessageId: str
22+
SigningCertUrl: HttpUrl
23+
Signature: str
24+
Timestamp: datetime
25+
SignatureVersion: str
26+
27+
28+
class SnsRecordModel(BaseModel):
29+
EventSource: Literal["aws:sns"]
30+
EventVersion: str
31+
EventSubscriptionArn: str
32+
Sns: SnsNotificationModel
33+
34+
35+
class SnsModel(BaseModel):
36+
Records: List[SnsRecordModel]

Diff for: tests/events/snsEvent.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"SignatureVersion": "1",
99
"Timestamp": "2019-01-02T12:45:07.000Z",
1010
"Signature": "tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==",
11-
"SigningCertUrl": "https://sns.us-east-2.amazonaws.com/SimpleNotificat ...",
11+
"SigningCertUrl": "https://sns.us-east-2.amazonaws.com/SimpleNotification",
1212
"MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
1313
"Message": "Hello from SNS!",
1414
"MessageAttributes": {
@@ -22,10 +22,10 @@
2222
}
2323
},
2424
"Type": "Notification",
25-
"UnsubscribeUrl": "https://sns.us-east-2.amazonaws.com/?Action=Unsubscri ...",
25+
"UnsubscribeUrl": "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe",
2626
"TopicArn": "arn:aws:sns:us-east-2:123456789012:sns-lambda",
2727
"Subject": "TestInvoke"
2828
}
2929
}
3030
]
31-
}
31+
}

Diff for: tests/functional/parser/schemas.py

+20
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
DynamoDBStreamModel,
99
DynamoDBStreamRecordModel,
1010
EventBridgeModel,
11+
SnsModel,
12+
SnsNotificationModel,
13+
SnsRecordModel,
1114
SqsModel,
1215
SqsRecordModel,
1316
)
@@ -51,3 +54,20 @@ class MyAdvancedSqsRecordModel(SqsRecordModel):
5154

5255
class MyAdvancedSqsBusiness(SqsModel):
5356
Records: List[MyAdvancedSqsRecordModel]
57+
58+
59+
class MySnsBusiness(BaseModel):
60+
message: str
61+
username: str
62+
63+
64+
class MySnsNotificationModel(SnsNotificationModel):
65+
Message: str
66+
67+
68+
class MyAdvancedSnsRecordModel(SnsRecordModel):
69+
Sns: MySnsNotificationModel
70+
71+
72+
class MyAdvancedSnsBusiness(SnsModel):
73+
Records: List[MyAdvancedSnsRecordModel]

Diff for: tests/functional/parser/test_sns.py

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from typing import Any, List
2+
3+
import pytest
4+
5+
from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser
6+
from aws_lambda_powertools.utilities.typing import LambdaContext
7+
from tests.functional.parser.schemas import MyAdvancedSnsBusiness, MySnsBusiness
8+
from tests.functional.parser.utils import load_event
9+
from tests.functional.validator.conftest import sns_event # noqa: F401
10+
11+
12+
@event_parser(model=MySnsBusiness, envelope=envelopes.SnsEnvelope)
13+
def handle_sns_json_body(event: List[MySnsBusiness], _: LambdaContext):
14+
assert len(event) == 1
15+
assert event[0].message == "hello world"
16+
assert event[0].username == "lessa"
17+
18+
19+
def test_handle_sns_trigger_event_json_body(sns_event): # noqa: F811
20+
handle_sns_json_body(sns_event, LambdaContext())
21+
22+
23+
def test_validate_event_does_not_conform_with_model():
24+
event: Any = {"invalid": "event"}
25+
26+
with pytest.raises(ValidationError):
27+
handle_sns_json_body(event, LambdaContext())
28+
29+
30+
def test_validate_event_does_not_conform_user_json_string_with_model():
31+
event: Any = {
32+
"Records": [
33+
{
34+
"EventVersion": "1.0",
35+
"EventSubscriptionArn": "arn:aws:sns:us-east-2:123456789012:sns-la ...",
36+
"EventSource": "aws:sns",
37+
"Sns": {
38+
"SignatureVersion": "1",
39+
"Timestamp": "2019-01-02T12:45:07.000Z",
40+
"Signature": "tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==",
41+
"SigningCertUrl": "https://sns.us-east-2.amazonaws.com/SimpleNotificat ...",
42+
"MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
43+
"Message": "not a valid JSON!",
44+
"MessageAttributes": {"Test": {"Type": "String", "Value": "TestString"}},
45+
"Type": "Notification",
46+
"UnsubscribeUrl": "https://sns.us-east-2.amazonaws.com/?Action=Unsubscri ...",
47+
"TopicArn": "arn:aws:sns:us-east-2:123456789012:sns-lambda",
48+
"Subject": "TestInvoke",
49+
},
50+
}
51+
]
52+
}
53+
54+
with pytest.raises(ValidationError):
55+
handle_sns_json_body(event, LambdaContext())
56+
57+
58+
@event_parser(model=MyAdvancedSnsBusiness)
59+
def handle_sns_no_envelope(event: MyAdvancedSnsBusiness, _: LambdaContext):
60+
records = event.Records
61+
record = records[0]
62+
63+
assert len(records) == 1
64+
assert record.EventVersion == "1.0"
65+
assert record.EventSubscriptionArn == "arn:aws:sns:us-east-2:123456789012:sns-la ..."
66+
assert record.EventSource == "aws:sns"
67+
assert record.Sns.Type == "Notification"
68+
assert record.Sns.UnsubscribeUrl.scheme == "https"
69+
assert record.Sns.UnsubscribeUrl.host == "sns.us-east-2.amazonaws.com"
70+
assert record.Sns.UnsubscribeUrl.query == "Action=Unsubscribe"
71+
assert record.Sns.TopicArn == "arn:aws:sns:us-east-2:123456789012:sns-lambda"
72+
assert record.Sns.Subject == "TestInvoke"
73+
assert record.Sns.SignatureVersion == "1"
74+
convert_time = int(round(record.Sns.Timestamp.timestamp() * 1000))
75+
assert convert_time == 1546433107000
76+
assert record.Sns.Signature == "tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r=="
77+
assert record.Sns.SigningCertUrl.host == "sns.us-east-2.amazonaws.com"
78+
assert record.Sns.SigningCertUrl.scheme == "https"
79+
assert record.Sns.SigningCertUrl.host == "sns.us-east-2.amazonaws.com"
80+
assert record.Sns.SigningCertUrl.path == "/SimpleNotification"
81+
assert record.Sns.MessageId == "95df01b4-ee98-5cb9-9903-4c221d41eb5e"
82+
assert record.Sns.Message == "Hello from SNS!"
83+
attrib_dict = record.Sns.MessageAttributes
84+
assert len(attrib_dict) == 2
85+
assert attrib_dict["Test"].Type == "String"
86+
assert attrib_dict["Test"].Value == "TestString"
87+
assert attrib_dict["TestBinary"].Type == "Binary"
88+
assert attrib_dict["TestBinary"].Value == "TestBinary"
89+
90+
91+
def test_handle_sns_trigger_event_no_envelope():
92+
event_dict = load_event("snsEvent.json")
93+
handle_sns_no_envelope(event_dict, LambdaContext())

Diff for: tests/functional/test_lambda_trigger_events.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -487,15 +487,15 @@ def test_sns_trigger_event():
487487
assert sns.signature_version == "1"
488488
assert sns.timestamp == "2019-01-02T12:45:07.000Z"
489489
assert sns.signature == "tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r=="
490-
assert sns.signing_cert_url == "https://sns.us-east-2.amazonaws.com/SimpleNotificat ..."
490+
assert sns.signing_cert_url == "https://sns.us-east-2.amazonaws.com/SimpleNotification"
491491
assert sns.message_id == "95df01b4-ee98-5cb9-9903-4c221d41eb5e"
492492
assert sns.message == "Hello from SNS!"
493493
message_attributes = sns.message_attributes
494494
test_message_attribute = message_attributes["Test"]
495495
assert test_message_attribute.get_type == "String"
496496
assert test_message_attribute.value == "TestString"
497497
assert sns.get_type == "Notification"
498-
assert sns.unsubscribe_url == "https://sns.us-east-2.amazonaws.com/?Action=Unsubscri ..."
498+
assert sns.unsubscribe_url == "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe"
499499
assert sns.topic_arn == "arn:aws:sns:us-east-2:123456789012:sns-lambda"
500500
assert sns.subject == "TestInvoke"
501501
assert event.record._data == event["Records"][0]

Diff for: tests/functional/validator/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,8 @@ def sns_event():
195195
"Timestamp": "1970-01-01T00:00:00.000Z",
196196
"SignatureVersion": "1",
197197
"Signature": "EXAMPLE",
198-
"SigningCertUrl": "EXAMPLE",
199-
"UnsubscribeUrl": "EXAMPLE",
198+
"SigningCertUrl": "https://www.example.com",
199+
"UnsubscribeUrl": "https://www.example.com",
200200
"MessageAttributes": {
201201
"Test": {"Type": "String", "Value": "TestString"},
202202
"TestBinary": {"Type": "Binary", "Value": "TestBinary"},

0 commit comments

Comments
 (0)