Skip to content

Commit c565705

Browse files
abhu85claude
andauthored
fix(sns): support SubscriptionConfirmation and UnsubscribeConfirmation message types (#1102)
* fix(sns): support SubscriptionConfirmation and UnsubscribeConfirmation message types The SnsMessage and SnsMessageObj structs were failing to deserialize SubscriptionConfirmation and UnsubscribeConfirmation SNS messages because: 1. `unsubscribe_url` was required (String) but these message types don't have it 2. `subscribe_url` and `token` fields were missing entirely This fix: - Makes `unsubscribe_url` optional (Option<String>) since it's only present in Notification messages - Adds `subscribe_url` field (Option<String>) for confirmation messages - Adds `token` field (Option<String>) for confirmation messages All fields use #[serde(default)] to handle missing fields gracefully. Fixes #966 Signed-off-by: abhu85 <151518127+abhu85@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 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> * docs(sns): remove ignore from SnsSubscriptionMessage example The doc example compiles as-is, so the ignore attribute is unnecessary. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Signed-off-by: abhu85 <151518127+abhu85@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: abhu85 <abhu85@users.noreply.github.com>
1 parent b3c4f9e commit c565705

3 files changed

Lines changed: 162 additions & 2 deletions

File tree

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

Lines changed: 135 additions & 2 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)]
@@ -115,6 +119,95 @@ pub struct SnsMessage {
115119
pub other: serde_json::Map<String, Value>,
116120
}
117121

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+
/// ```
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.
165+
#[serde(default)]
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,
176+
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.
182+
///
183+
/// For UnsubscribeConfirmation messages, this field will be `None`.
184+
#[serde(alias = "SubscribeURL")]
185+
#[serde(default)]
186+
pub subscribe_url: Option<String>,
187+
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,
192+
193+
/// The Message value containing a description of the subscription confirmation.
194+
pub message: String,
195+
196+
/// 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)
197+
#[serde(deserialize_with = "deserialize_lambda_map")]
198+
#[serde(default)]
199+
pub message_attributes: HashMap<String, MessageAttribute>,
200+
201+
/// Catchall to catch any additional fields that were present but not explicitly defined by this struct.
202+
/// Enabled with Cargo feature `catch-all-fields`.
203+
/// If `catch-all-fields` is disabled, any additional fields that are present will be ignored.
204+
#[cfg(feature = "catch-all-fields")]
205+
#[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))]
206+
#[serde(flatten)]
207+
#[cfg_attr(feature = "builders", builder(default))]
208+
pub other: serde_json::Map<String, Value>,
209+
}
210+
118211
/// An alternate `Event` notification event to use alongside `SnsRecordObj<T>` and `SnsMessageObj<T>` if you want to deserialize an object inside your SNS messages rather than getting an `Option<String>` message
119212
///
120213
/// [https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html)
@@ -165,7 +258,11 @@ pub struct SnsRecordObj<T: Serialize> {
165258
pub other: serde_json::Map<String, Value>,
166259
}
167260

168-
/// 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.
169266
#[non_exhaustive]
170267
#[cfg_attr(feature = "builders", derive(Builder))]
171268
#[serde_with::serde_as]
@@ -462,4 +559,40 @@ mod test {
462559
let reparsed: SnsEventObj<CustStruct> = serde_json::from_slice(output.as_bytes()).unwrap();
463560
assert_eq!(parsed, reparsed);
464561
}
562+
563+
#[test]
564+
#[cfg(feature = "sns")]
565+
fn my_example_sns_subscription_confirmation() {
566+
// Test for issue #966: SnsSubscriptionMessage for SubscriptionConfirmation types
567+
let data = include_bytes!("../../fixtures/example-sns-subscription-confirmation.json");
568+
let parsed: SnsSubscriptionMessage = serde_json::from_slice(data).unwrap();
569+
570+
assert_eq!("SubscriptionConfirmation", parsed.sns_message_type);
571+
assert!(parsed.subscribe_url.is_some());
572+
assert_eq!(
573+
"https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-1:123456789012:MyTopic&Token=2336412f37fb687f5d51e6e2425dacbbffff",
574+
parsed.subscribe_url.as_ref().unwrap()
575+
);
576+
assert_eq!("2336412f37fb687f5d51e6e2425dacbbffff", parsed.token);
577+
578+
let output: String = serde_json::to_string(&parsed).unwrap();
579+
let reparsed: SnsSubscriptionMessage = serde_json::from_slice(output.as_bytes()).unwrap();
580+
assert_eq!(parsed, reparsed);
581+
}
582+
583+
#[test]
584+
#[cfg(feature = "sns")]
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);
597+
}
465598
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
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": {}
14+
}
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)