Skip to content

Commit 86cac61

Browse files
abhu85claude
andcommitted
feat(sns): add SnsSubscriptionMessage for confirmation message types
Add a separate SnsSubscriptionMessage struct for handling SubscriptionConfirmation and UnsubscribeConfirmation SNS message types. This is a non-breaking change that keeps the existing SnsMessage struct unchanged. Changes: - Add SnsSubscriptionMessage struct with subscribe_url (Option) and token fields - Add documentation to SnsMessage/SnsMessageObj clarifying they are for Notification only - Add test fixtures for both SubscriptionConfirmation and UnsubscribeConfirmation - Add tests verifying deserialization of both confirmation types The new struct distinguishes confirmation types by: - sns_message_type field ("SubscriptionConfirmation" or "UnsubscribeConfirmation") - subscribe_url: Some(url) for SubscriptionConfirmation, None for UnsubscribeConfirmation Fixes #966 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ee8d6e1 commit 86cac61

3 files changed

Lines changed: 139 additions & 75 deletions

File tree

lambda-events/src/event/sns/mod.rs

Lines changed: 114 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ pub struct SnsRecord {
5656
pub other: serde_json::Map<String, Value>,
5757
}
5858

59-
/// SnsMessage stores information about each record of a SNS event
59+
/// SnsMessage stores information about SNS **Notification** type messages only.
60+
///
61+
/// **Important**: This struct is designed specifically for handling SNS Notification messages
62+
/// (where `Type` field equals "Notification"). For handling SubscriptionConfirmation or
63+
/// UnsubscribeConfirmation messages, use [`SnsSubscriptionMessage`] instead.
6064
#[non_exhaustive]
6165
#[cfg_attr(feature = "builders", derive(Builder))]
6266
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
@@ -94,26 +98,99 @@ pub struct SnsMessage {
9498
pub signing_cert_url: String,
9599

96100
/// A URL that you can use to unsubscribe the endpoint from this topic. If you visit this URL, Amazon SNS unsubscribes the endpoint and stops sending notifications to this endpoint.
97-
///
98-
/// Note: This field is only present in Notification messages. It is not present in SubscriptionConfirmation or UnsubscribeConfirmation messages.
99101
#[serde(alias = "UnsubscribeURL")]
102+
pub unsubscribe_url: String,
103+
104+
/// The Message value specified when the notification was published to the topic.
105+
pub message: String,
106+
107+
/// This is a HashMap of defined attributes for a message. Additional details can be found in the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html)
108+
#[serde(deserialize_with = "deserialize_lambda_map")]
109+
#[serde(default)]
110+
pub message_attributes: HashMap<String, MessageAttribute>,
111+
112+
/// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
113+
/// Enabled with Cargo feature `catch-all-fields`.
114+
/// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
115+
#[cfg(feature = "catch-all-fields")]
116+
#[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
117+
#[serde(flatten)]
118+
#[cfg_attr(feature = "builders", builder(default))]
119+
pub other: serde_json::Map<String, Value>,
120+
}
121+
122+
/// SnsSubscriptionMessage stores information about SNS SubscriptionConfirmation and
123+
/// UnsubscribeConfirmation type messages.
124+
///
125+
/// Use this struct when handling messages where the `Type` field equals "SubscriptionConfirmation"
126+
/// or "UnsubscribeConfirmation". For handling Notification messages, use [`SnsMessage`] instead.
127+
///
128+
/// # Distinguishing SubscriptionConfirmation from UnsubscribeConfirmation
129+
///
130+
/// Both message types use this same struct. You can distinguish them by:
131+
/// - Checking the `sns_message_type` field ("SubscriptionConfirmation" or "UnsubscribeConfirmation")
132+
/// - Checking `subscribe_url`: `Some(url)` for SubscriptionConfirmation, `None` for UnsubscribeConfirmation
133+
///
134+
/// # Example
135+
///
136+
/// ```ignore
137+
/// use aws_lambda_events::event::sns::SnsSubscriptionMessage;
138+
///
139+
/// fn handle_confirmation(msg: SnsSubscriptionMessage) {
140+
/// if let Some(url) = &msg.subscribe_url {
141+
/// // SubscriptionConfirmation - visit URL or use token to confirm
142+
/// println!("Confirm subscription at: {}", url);
143+
/// } else {
144+
/// // UnsubscribeConfirmation
145+
/// println!("Unsubscribe confirmed");
146+
/// }
147+
/// }
148+
/// ```
149+
#[non_exhaustive]
150+
#[cfg_attr(feature = "builders", derive(Builder))]
151+
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
152+
#[serde(rename_all = "PascalCase")]
153+
pub struct SnsSubscriptionMessage {
154+
/// The type of SNS message. Will be "SubscriptionConfirmation" or "UnsubscribeConfirmation".
155+
#[serde(rename = "Type")]
156+
pub sns_message_type: String,
157+
158+
/// A Universally Unique Identifier, unique for each message published.
159+
pub message_id: String,
160+
161+
/// The Amazon Resource Name (ARN) for the topic that this message was published to.
162+
pub topic_arn: String,
163+
164+
/// The Subject parameter specified when the notification was published to the topic.
100165
#[serde(default)]
101-
pub unsubscribe_url: Option<String>,
166+
pub subject: Option<String>,
167+
168+
/// The time (UTC) when the message was sent.
169+
pub timestamp: DateTime<Utc>,
170+
171+
/// Version of the Amazon SNS signature used.
172+
pub signature_version: String,
173+
174+
/// Base64-encoded SHA1withRSA signature of the Message, MessageId, Subject (if present), Type, Timestamp, and TopicArn values.
175+
pub signature: String,
102176

103-
/// A URL that you can visit to re-confirm the subscription or confirm the unsubscription.
177+
/// The URL to the certificate that was used to sign the message.
178+
#[serde(alias = "SigningCertURL")]
179+
pub signing_cert_url: String,
180+
181+
/// A URL that you can visit to confirm the subscription. Present only for SubscriptionConfirmation messages.
104182
///
105-
/// Note: This field is only present in SubscriptionConfirmation and UnsubscribeConfirmation messages.
183+
/// For UnsubscribeConfirmation messages, this field will be `None`.
106184
#[serde(alias = "SubscribeURL")]
107185
#[serde(default)]
108186
pub subscribe_url: Option<String>,
109187

110-
/// A value you can use with the ConfirmSubscription action to re-confirm the subscription.
111-
///
112-
/// Note: This field is only present in SubscriptionConfirmation and UnsubscribeConfirmation messages.
113-
#[serde(default)]
114-
pub token: Option<String>,
188+
/// A value you can use with the ConfirmSubscription action to confirm the subscription.
189+
/// Alternatively, you can simply visit the `subscribe_url`.
190+
#[serde(rename = "Token")]
191+
pub token: String,
115192

116-
/// The Message value specified when the notification was published to the topic.
193+
/// The Message value containing a description of the subscription confirmation.
117194
pub message: String,
118195

119196
/// This is a HashMap of defined attributes for a message. Additional details can be found in the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html)
@@ -181,7 +258,11 @@ pub struct SnsRecordObj<T: Serialize> {
181258
pub other: serde_json::Map<String, Value>,
182259
}
183260

184-
/// Alternate version of `SnsMessage` to use in conjunction with `SnsEventObj<T>` and `SnsRecordObj<T>` for deserializing the message into a struct of type `T`
261+
/// Alternate version of `SnsMessage` to use in conjunction with `SnsEventObj<T>` and `SnsRecordObj<T>` for deserializing the message into a struct of type `T`.
262+
///
263+
/// **Important**: This struct is designed specifically for handling SNS Notification messages
264+
/// (where `Type` field equals "Notification"). For handling SubscriptionConfirmation or
265+
/// UnsubscribeConfirmation messages, use [`SnsSubscriptionMessage`] instead.
185266
#[non_exhaustive]
186267
#[cfg_attr(feature = "builders", derive(Builder))]
187268
#[serde_with::serde_as]
@@ -221,24 +302,8 @@ pub struct SnsMessageObj<T: Serialize> {
221302
pub signing_cert_url: String,
222303

223304
/// A URL that you can use to unsubscribe the endpoint from this topic. If you visit this URL, Amazon SNS unsubscribes the endpoint and stops sending notifications to this endpoint.
224-
///
225-
/// Note: This field is only present in Notification messages. It is not present in SubscriptionConfirmation or UnsubscribeConfirmation messages.
226305
#[serde(alias = "UnsubscribeURL")]
227-
#[serde(default)]
228-
pub unsubscribe_url: Option<String>,
229-
230-
/// A URL that you can visit to re-confirm the subscription or confirm the unsubscription.
231-
///
232-
/// Note: This field is only present in SubscriptionConfirmation and UnsubscribeConfirmation messages.
233-
#[serde(alias = "SubscribeURL")]
234-
#[serde(default)]
235-
pub subscribe_url: Option<String>,
236-
237-
/// A value you can use with the ConfirmSubscription action to re-confirm the subscription.
238-
///
239-
/// Note: This field is only present in SubscriptionConfirmation and UnsubscribeConfirmation messages.
240-
#[serde(default)]
241-
pub token: Option<String>,
306+
pub unsubscribe_url: String,
242307

243308
/// Deserialized into a `T` from nested JSON inside the SNS message string. `T` must implement the `Deserialize` or `DeserializeOwned` trait.
244309
#[serde_as(as = "serde_with::json::JsonString")]
@@ -498,41 +563,36 @@ mod test {
498563
#[test]
499564
#[cfg(feature = "sns")]
500565
fn my_example_sns_subscription_confirmation() {
501-
// Test for issue #966: SnsMessage struct fails with SubscriptionConfirmation types
502-
// SubscriptionConfirmation messages have SubscribeURL and Token fields instead of UnsubscribeURL
566+
// Test for issue #966: SnsSubscriptionMessage for SubscriptionConfirmation types
503567
let data = include_bytes!("../../fixtures/example-sns-subscription-confirmation.json");
504-
let parsed: SnsEvent = serde_json::from_slice(data).unwrap();
505-
assert_eq!(1, parsed.records.len());
568+
let parsed: SnsSubscriptionMessage = serde_json::from_slice(data).unwrap();
506569

507-
let sns_message = &parsed.records[0].sns;
508-
assert_eq!("SubscriptionConfirmation", sns_message.sns_message_type);
509-
assert!(sns_message.unsubscribe_url.is_none());
510-
assert!(sns_message.subscribe_url.is_some());
511-
assert!(sns_message.token.is_some());
570+
assert_eq!("SubscriptionConfirmation", parsed.sns_message_type);
571+
assert!(parsed.subscribe_url.is_some());
512572
assert_eq!(
513573
"https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-1:123456789012:MyTopic&Token=2336412f37fb687f5d51e6e2425dacbbffff",
514-
sns_message.subscribe_url.as_ref().unwrap()
515-
);
516-
assert_eq!(
517-
"2336412f37fb687f5d51e6e2425dacbbffff",
518-
sns_message.token.as_ref().unwrap()
574+
parsed.subscribe_url.as_ref().unwrap()
519575
);
576+
assert_eq!("2336412f37fb687f5d51e6e2425dacbbffff", parsed.token);
520577

521578
let output: String = serde_json::to_string(&parsed).unwrap();
522-
let reparsed: SnsEvent = serde_json::from_slice(output.as_bytes()).unwrap();
579+
let reparsed: SnsSubscriptionMessage = serde_json::from_slice(output.as_bytes()).unwrap();
523580
assert_eq!(parsed, reparsed);
524581
}
525582

526583
#[test]
527584
#[cfg(feature = "sns")]
528-
fn my_example_sns_notification_has_unsubscribe_url() {
529-
// Verify that Notification messages still have unsubscribe_url
530-
let data = include_bytes!("../../fixtures/example-sns-event.json");
531-
let parsed: SnsEvent = serde_json::from_slice(data).unwrap();
532-
let sns_message = &parsed.records[0].sns;
533-
assert_eq!("Notification", sns_message.sns_message_type);
534-
assert!(sns_message.unsubscribe_url.is_some());
535-
assert!(sns_message.subscribe_url.is_none());
536-
assert!(sns_message.token.is_none());
585+
fn my_example_sns_unsubscribe_confirmation() {
586+
// Test for UnsubscribeConfirmation messages - subscribe_url should be None
587+
let data = include_bytes!("../../fixtures/example-sns-unsubscribe-confirmation.json");
588+
let parsed: SnsSubscriptionMessage = serde_json::from_slice(data).unwrap();
589+
590+
assert_eq!("UnsubscribeConfirmation", parsed.sns_message_type);
591+
assert!(parsed.subscribe_url.is_none());
592+
assert_eq!("2336412f37fb687f5d51e6e2425dacbbeeee", parsed.token);
593+
594+
let output: String = serde_json::to_string(&parsed).unwrap();
595+
let reparsed: SnsSubscriptionMessage = serde_json::from_slice(output.as_bytes()).unwrap();
596+
assert_eq!(parsed, reparsed);
537597
}
538598
}
Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
11
{
2-
"Records": [
3-
{
4-
"EventVersion": "1.0",
5-
"EventSubscriptionArn": "arn:aws:sns:us-east-1:123456789012:MyTopic:00000000-0000-0000-0000-000000000000",
6-
"EventSource": "aws:sns",
7-
"Sns": {
8-
"Type": "SubscriptionConfirmation",
9-
"MessageId": "165545c9-2a5c-472c-8df2-7ff2be2b3b1b",
10-
"TopicArn": "arn:aws:sns:us-east-1:123456789012:MyTopic",
11-
"Message": "You have chosen to subscribe to the topic arn:aws:sns:us-east-1:123456789012:MyTopic.\nTo confirm the subscription, visit the SubscribeURL included in this message.",
12-
"Timestamp": "2012-04-26T20:45:04.751Z",
13-
"SignatureVersion": "1",
14-
"Signature": "EXAMPLEpH+DcEwjAPg8O9mY8dReBSwksfg2S7WKQcikcNKWLQjwu6A4VbeS0QHVCkhRS7fUQvi2egU3N858fiTDN6bkkOxYDVrY0Ad8L10Hs3zH81mtnPk5uvvolIC1CXGu43obcgFxeL3khZl8IKvO61GWB6jI9b5+gLPoBc1Q=",
15-
"SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem",
16-
"SubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-1:123456789012:MyTopic&Token=2336412f37fb687f5d51e6e2425dacbbffff",
17-
"Token": "2336412f37fb687f5d51e6e2425dacbbffff",
18-
"Subject": null,
19-
"MessageAttributes": {}
20-
}
21-
}
22-
]
2+
"Type": "SubscriptionConfirmation",
3+
"MessageId": "165545c9-2a5c-472c-8df2-7ff2be2b3b1b",
4+
"TopicArn": "arn:aws:sns:us-east-1:123456789012:MyTopic",
5+
"Message": "You have chosen to subscribe to the topic arn:aws:sns:us-east-1:123456789012:MyTopic.\nTo confirm the subscription, visit the SubscribeURL included in this message.",
6+
"Timestamp": "2012-04-26T20:45:04.751Z",
7+
"SignatureVersion": "1",
8+
"Signature": "EXAMPLEpH+DcEwjAPg8O9mY8dReBSwksfg2S7WKQcikcNKWLQjwu6A4VbeS0QHVCkhRS7fUQvi2egU3N858fiTDN6bkkOxYDVrY0Ad8L10Hs3zH81mtnPk5uvvolIC1CXGu43obcgFxeL3khZl8IKvO61GWB6jI9b5+gLPoBc1Q=",
9+
"SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem",
10+
"SubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-1:123456789012:MyTopic&Token=2336412f37fb687f5d51e6e2425dacbbffff",
11+
"Token": "2336412f37fb687f5d51e6e2425dacbbffff",
12+
"Subject": null,
13+
"MessageAttributes": {}
2314
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"Type": "UnsubscribeConfirmation",
3+
"MessageId": "47138184-6831-46b8-8466-7168d3b90898",
4+
"TopicArn": "arn:aws:sns:us-east-1:123456789012:MyTopic",
5+
"Message": "You have chosen to deactivate subscription arn:aws:sns:us-east-1:123456789012:MyTopic:00000000-0000-0000-0000-000000000000.\nTo cancel this operation and restore the subscription, visit the SubscribeURL included in this message.",
6+
"Timestamp": "2012-04-26T20:45:04.751Z",
7+
"SignatureVersion": "1",
8+
"Signature": "EXAMPLEpH+DcEwjAPg8O9mY8dReBSwksfg2S7WKQcikcNKWLQjwu6A4VbeS0QHVCkhRS7fUQvi2egU3N858fiTDN6bkkOxYDVrY0Ad8L10Hs3zH81mtnPk5uvvolIC1CXGu43obcgFxeL3khZl8IKvO61GWB6jI9b5+gLPoBc1Q=",
9+
"SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem",
10+
"Token": "2336412f37fb687f5d51e6e2425dacbbeeee",
11+
"Subject": null,
12+
"MessageAttributes": {}
13+
}

0 commit comments

Comments
 (0)