diff --git a/lambda-events/src/event/sns/mod.rs b/lambda-events/src/event/sns/mod.rs index 3e752259..a8bc205e 100644 --- a/lambda-events/src/event/sns/mod.rs +++ b/lambda-events/src/event/sns/mod.rs @@ -56,7 +56,11 @@ pub struct SnsRecord { pub other: serde_json::Map, } -/// SnsMessage stores information about each record of a SNS event +/// SnsMessage stores information about SNS **Notification** type messages only. +/// +/// **Important**: This struct is designed specifically for handling SNS Notification messages +/// (where `Type` field equals "Notification"). For handling SubscriptionConfirmation or +/// UnsubscribeConfirmation messages, use [`SnsSubscriptionMessage`] instead. #[non_exhaustive] #[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] @@ -115,6 +119,95 @@ pub struct SnsMessage { pub other: serde_json::Map, } +/// SnsSubscriptionMessage stores information about SNS SubscriptionConfirmation and +/// UnsubscribeConfirmation type messages. +/// +/// Use this struct when handling messages where the `Type` field equals "SubscriptionConfirmation" +/// or "UnsubscribeConfirmation". For handling Notification messages, use [`SnsMessage`] instead. +/// +/// # Distinguishing SubscriptionConfirmation from UnsubscribeConfirmation +/// +/// Both message types use this same struct. You can distinguish them by: +/// - Checking the `sns_message_type` field ("SubscriptionConfirmation" or "UnsubscribeConfirmation") +/// - Checking `subscribe_url`: `Some(url)` for SubscriptionConfirmation, `None` for UnsubscribeConfirmation +/// +/// # Example +/// +/// ``` +/// use aws_lambda_events::event::sns::SnsSubscriptionMessage; +/// +/// fn handle_confirmation(msg: SnsSubscriptionMessage) { +/// if let Some(url) = &msg.subscribe_url { +/// // SubscriptionConfirmation - visit URL or use token to confirm +/// println!("Confirm subscription at: {}", url); +/// } else { +/// // UnsubscribeConfirmation +/// println!("Unsubscribe confirmed"); +/// } +/// } +/// ``` +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct SnsSubscriptionMessage { + /// The type of SNS message. Will be "SubscriptionConfirmation" or "UnsubscribeConfirmation". + #[serde(rename = "Type")] + pub sns_message_type: String, + + /// A Universally Unique Identifier, unique for each message published. + pub message_id: String, + + /// The Amazon Resource Name (ARN) for the topic that this message was published to. + pub topic_arn: String, + + /// The Subject parameter specified when the notification was published to the topic. + #[serde(default)] + pub subject: Option, + + /// The time (UTC) when the message was sent. + pub timestamp: DateTime, + + /// Version of the Amazon SNS signature used. + pub signature_version: String, + + /// Base64-encoded SHA1withRSA signature of the Message, MessageId, Subject (if present), Type, Timestamp, and TopicArn values. + pub signature: String, + + /// The URL to the certificate that was used to sign the message. + #[serde(alias = "SigningCertURL")] + pub signing_cert_url: String, + + /// A URL that you can visit to confirm the subscription. Present only for SubscriptionConfirmation messages. + /// + /// For UnsubscribeConfirmation messages, this field will be `None`. + #[serde(alias = "SubscribeURL")] + #[serde(default)] + pub subscribe_url: Option, + + /// A value you can use with the ConfirmSubscription action to confirm the subscription. + /// Alternatively, you can simply visit the `subscribe_url`. + #[serde(rename = "Token")] + pub token: String, + + /// The Message value containing a description of the subscription confirmation. + pub message: String, + + /// 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) + #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(default)] + pub message_attributes: HashMap, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + /// An alternate `Event` notification event to use alongside `SnsRecordObj` and `SnsMessageObj` if you want to deserialize an object inside your SNS messages rather than getting an `Option` message /// /// [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 { pub other: serde_json::Map, } -/// Alternate version of `SnsMessage` to use in conjunction with `SnsEventObj` and `SnsRecordObj` for deserializing the message into a struct of type `T` +/// Alternate version of `SnsMessage` to use in conjunction with `SnsEventObj` and `SnsRecordObj` for deserializing the message into a struct of type `T`. +/// +/// **Important**: This struct is designed specifically for handling SNS Notification messages +/// (where `Type` field equals "Notification"). For handling SubscriptionConfirmation or +/// UnsubscribeConfirmation messages, use [`SnsSubscriptionMessage`] instead. #[non_exhaustive] #[cfg_attr(feature = "builders", derive(Builder))] #[serde_with::serde_as] @@ -462,4 +559,40 @@ mod test { let reparsed: SnsEventObj = serde_json::from_slice(output.as_bytes()).unwrap(); assert_eq!(parsed, reparsed); } + + #[test] + #[cfg(feature = "sns")] + fn my_example_sns_subscription_confirmation() { + // Test for issue #966: SnsSubscriptionMessage for SubscriptionConfirmation types + let data = include_bytes!("../../fixtures/example-sns-subscription-confirmation.json"); + let parsed: SnsSubscriptionMessage = serde_json::from_slice(data).unwrap(); + + assert_eq!("SubscriptionConfirmation", parsed.sns_message_type); + assert!(parsed.subscribe_url.is_some()); + assert_eq!( + "https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-1:123456789012:MyTopic&Token=2336412f37fb687f5d51e6e2425dacbbffff", + parsed.subscribe_url.as_ref().unwrap() + ); + assert_eq!("2336412f37fb687f5d51e6e2425dacbbffff", parsed.token); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SnsSubscriptionMessage = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "sns")] + fn my_example_sns_unsubscribe_confirmation() { + // Test for UnsubscribeConfirmation messages - subscribe_url should be None + let data = include_bytes!("../../fixtures/example-sns-unsubscribe-confirmation.json"); + let parsed: SnsSubscriptionMessage = serde_json::from_slice(data).unwrap(); + + assert_eq!("UnsubscribeConfirmation", parsed.sns_message_type); + assert!(parsed.subscribe_url.is_none()); + assert_eq!("2336412f37fb687f5d51e6e2425dacbbeeee", parsed.token); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SnsSubscriptionMessage = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } } diff --git a/lambda-events/src/fixtures/example-sns-subscription-confirmation.json b/lambda-events/src/fixtures/example-sns-subscription-confirmation.json new file mode 100644 index 00000000..563cf2b0 --- /dev/null +++ b/lambda-events/src/fixtures/example-sns-subscription-confirmation.json @@ -0,0 +1,14 @@ +{ + "Type": "SubscriptionConfirmation", + "MessageId": "165545c9-2a5c-472c-8df2-7ff2be2b3b1b", + "TopicArn": "arn:aws:sns:us-east-1:123456789012:MyTopic", + "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.", + "Timestamp": "2012-04-26T20:45:04.751Z", + "SignatureVersion": "1", + "Signature": "EXAMPLEpH+DcEwjAPg8O9mY8dReBSwksfg2S7WKQcikcNKWLQjwu6A4VbeS0QHVCkhRS7fUQvi2egU3N858fiTDN6bkkOxYDVrY0Ad8L10Hs3zH81mtnPk5uvvolIC1CXGu43obcgFxeL3khZl8IKvO61GWB6jI9b5+gLPoBc1Q=", + "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem", + "SubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-1:123456789012:MyTopic&Token=2336412f37fb687f5d51e6e2425dacbbffff", + "Token": "2336412f37fb687f5d51e6e2425dacbbffff", + "Subject": null, + "MessageAttributes": {} +} diff --git a/lambda-events/src/fixtures/example-sns-unsubscribe-confirmation.json b/lambda-events/src/fixtures/example-sns-unsubscribe-confirmation.json new file mode 100644 index 00000000..88035569 --- /dev/null +++ b/lambda-events/src/fixtures/example-sns-unsubscribe-confirmation.json @@ -0,0 +1,13 @@ +{ + "Type": "UnsubscribeConfirmation", + "MessageId": "47138184-6831-46b8-8466-7168d3b90898", + "TopicArn": "arn:aws:sns:us-east-1:123456789012:MyTopic", + "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.", + "Timestamp": "2012-04-26T20:45:04.751Z", + "SignatureVersion": "1", + "Signature": "EXAMPLEpH+DcEwjAPg8O9mY8dReBSwksfg2S7WKQcikcNKWLQjwu6A4VbeS0QHVCkhRS7fUQvi2egU3N858fiTDN6bkkOxYDVrY0Ad8L10Hs3zH81mtnPk5uvvolIC1CXGu43obcgFxeL3khZl8IKvO61GWB6jI9b5+gLPoBc1Q=", + "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem", + "Token": "2336412f37fb687f5d51e6e2425dacbbeeee", + "Subject": null, + "MessageAttributes": {} +}