1use std::collections::HashMap;
2use std::sync::Arc;
3
4pub struct DeliveryBus {
10 sqs_sender: Option<Arc<dyn SqsDelivery>>,
12 sns_sender: Option<Arc<dyn SnsDelivery>>,
14 eventbridge_sender: Option<Arc<dyn EventBridgeDelivery>>,
16 lambda_invoker: Option<Arc<dyn LambdaDelivery>>,
18 kinesis_sender: Option<Arc<dyn KinesisDelivery>>,
20 stepfunctions_starter: Option<Arc<dyn StepFunctionsDelivery>>,
22 sagemaker_pipeline_starter: Option<Arc<dyn SageMakerPipelineDelivery>>,
24 s3_writer: Option<Arc<dyn S3Delivery>>,
26 firehose_sender: Option<Arc<dyn FirehoseDelivery>>,
28 ses_dispatcher: Option<Arc<dyn SesSendEmailDispatcher>>,
30 ecs_task_runner: Option<Arc<dyn EcsTaskRunner>>,
32 elbv2_target_registration: Option<Arc<dyn Elbv2TargetRegistration>>,
34 ec2_network_lookup: Option<Arc<dyn Ec2NetworkLookup>>,
35 cloudwatch_metrics: Option<Arc<dyn CloudwatchDelivery>>,
38 cloudwatch_logs: Option<Arc<dyn CloudwatchLogsDelivery>>,
41 cognito_jwt_verifier: Option<Arc<dyn CognitoJwtVerifier>>,
45 kms_hook: Option<Arc<dyn KmsHook>>,
48}
49
50#[derive(Debug, Clone)]
52pub struct SqsMessageAttribute {
53 pub data_type: String,
54 pub string_value: Option<String>,
55 pub binary_value: Option<String>,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum SqsDeliveryError {
64 QueueNotFound(String),
66 InvalidArn(String),
68 InvalidParameter(String),
73 AccessDenied(String),
77 TargetUnavailable(String),
80 Kms(KmsError),
85}
86
87impl std::fmt::Display for SqsDeliveryError {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 match self {
90 Self::QueueNotFound(arn) => write!(f, "queue not found: {arn}"),
91 Self::InvalidArn(arn) => write!(f, "invalid queue ARN: {arn}"),
92 Self::InvalidParameter(msg) => write!(f, "invalid parameter: {msg}"),
93 Self::AccessDenied(msg) => write!(f, "access denied: {msg}"),
94 Self::TargetUnavailable(msg) => write!(f, "target unavailable: {msg}"),
95 Self::Kms(err) => write!(f, "KMS refused the delivery: {err}"),
96 }
97 }
98}
99
100impl std::error::Error for SqsDeliveryError {}
101
102impl SqsDeliveryError {
103 pub fn relayed_message(&self) -> String {
110 match self {
111 Self::QueueNotFound(_) => format!(
112 "The specified queue does not exist or you do not have access to it. (Service: AmazonSQS; Status Code: 400; Error Code: AWS.SimpleQueueService.NonExistentQueue; Request ID: {}; Proxy: null)",
113 uuid::Uuid::new_v4()
114 ),
115 Self::Kms(err) => {
116 let (status, code) = err.kind.sqs_code();
117 format!(
118 "{} (Service: AmazonSQS; Status Code: {status}; Error Code: {code}; Request ID: {}; Proxy: null)",
119 err.relayed_message(),
120 uuid::Uuid::new_v4()
121 )
122 }
123 other => other.to_string(),
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum DeliverySender<'a> {
141 Service {
144 principal: &'a str,
145 source_arn: &'a str,
146 source_account: &'a str,
147 },
148 Identity { arn: Option<&'a str> },
151}
152
153pub trait SqsDelivery: Send + Sync {
155 fn deliver_to_queue(
156 &self,
157 sender: DeliverySender<'_>,
158 queue_arn: &str,
159 message_body: &str,
160 attributes: &HashMap<String, String>,
161 );
162
163 fn deliver_to_queue_with_attrs(
165 &self,
166 sender: DeliverySender<'_>,
167 queue_arn: &str,
168 message_body: &str,
169 message_attributes: &HashMap<String, SqsMessageAttribute>,
170 message_group_id: Option<&str>,
171 message_dedup_id: Option<&str>,
172 ) {
173 let _ = (message_attributes, message_group_id, message_dedup_id);
175 self.deliver_to_queue(sender, queue_arn, message_body, &HashMap::new());
176 }
177
178 fn check_sender(
184 &self,
185 sender: DeliverySender<'_>,
186 queue_arn: &str,
187 ) -> Result<(), SqsDeliveryError> {
188 let _ = (sender, queue_arn);
189 Ok(())
190 }
191
192 fn queue_arn_for_url(&self, region: &str, queue_url: &str) -> Option<String> {
199 let _ = (region, queue_url);
200 None
201 }
202
203 #[allow(clippy::too_many_arguments)]
209 fn try_deliver_to_queue_with_attrs(
210 &self,
211 sender: DeliverySender<'_>,
212 queue_arn: &str,
213 message_body: &str,
214 message_attributes: &HashMap<String, SqsMessageAttribute>,
215 message_group_id: Option<&str>,
216 message_dedup_id: Option<&str>,
217 ) -> Result<(), SqsDeliveryError> {
218 self.deliver_to_queue_with_attrs(
219 sender,
220 queue_arn,
221 message_body,
222 message_attributes,
223 message_group_id,
224 message_dedup_id,
225 );
226 Ok(())
227 }
228}
229
230pub trait SnsDelivery: Send + Sync {
232 fn publish_to_topic(&self, topic_arn: &str, message: &str, subject: Option<&str>);
233
234 fn publish_to_topic_fifo(
238 &self,
239 topic_arn: &str,
240 message: &str,
241 subject: Option<&str>,
242 _message_group_id: Option<&str>,
243 _message_dedup_id: Option<&str>,
244 ) {
245 self.publish_to_topic(topic_arn, message, subject);
246 }
247
248 fn publish_to_topic_with_attributes(
254 &self,
255 topic_arn: &str,
256 message: &str,
257 subject: Option<&str>,
258 attributes: &HashMap<String, SqsMessageAttribute>,
259 ) {
260 let _ = (message, subject, attributes);
261 tracing::warn!(
262 topic_arn,
263 "SNS delivery does not support message attributes; message not published"
264 );
265 }
266}
267
268#[derive(Debug, Clone, Copy)]
280pub struct CrossServiceEvent<'a> {
281 pub source: &'a str,
282 pub detail_type: &'a str,
283 pub detail: &'a str,
285 pub event_bus: &'a str,
289 pub account_id: &'a str,
291 pub region: &'a str,
293 pub resources: &'a [String],
295 pub principal_arn: Option<&'a str>,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
304pub enum EventBridgeDeliveryError {
305 #[error("AccessDeniedException: {0}")]
309 AccessDenied(String),
310 #[error("InternalException: {0}")]
313 Unavailable(String),
314}
315
316pub trait EventBridgeDelivery: Send + Sync {
318 fn put_event(&self, event: &CrossServiceEvent<'_>) -> Result<String, EventBridgeDeliveryError>;
322}
323
324pub const FUNCTION_ERROR_UNHANDLED: &str = "Unhandled";
329
330#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct LambdaInvocationOutput {
339 pub payload: Vec<u8>,
342 pub function_error: Option<String>,
344}
345
346impl LambdaInvocationOutput {
347 pub fn success(payload: impl Into<Vec<u8>>) -> Self {
349 Self {
350 payload: payload.into(),
351 function_error: None,
352 }
353 }
354
355 pub fn unhandled(payload: impl Into<Vec<u8>>) -> Self {
357 Self {
358 payload: payload.into(),
359 function_error: Some(FUNCTION_ERROR_UNHANDLED.to_string()),
360 }
361 }
362
363 pub fn is_function_error(&self) -> bool {
365 self.function_error.is_some()
366 }
367
368 pub fn error_type(&self) -> Option<String> {
371 self.error_field("errorType")
372 }
373
374 pub fn error_message(&self) -> Option<String> {
377 self.error_field("errorMessage")
378 }
379
380 fn error_field(&self, key: &str) -> Option<String> {
381 if !self.is_function_error() {
382 return None;
383 }
384 serde_json::from_slice::<serde_json::Value>(&self.payload)
385 .ok()?
386 .get(key)?
387 .as_str()
388 .map(str::to_string)
389 }
390}
391
392pub type AsyncUnrunnableHook = Box<dyn FnOnce(String) + Send>;
396
397pub trait LambdaDelivery: Send + Sync {
398 fn invoke_lambda(
405 &self,
406 function_arn: &str,
407 payload: &str,
408 ) -> std::pin::Pin<
409 Box<dyn std::future::Future<Output = Result<LambdaInvocationOutput, String>> + Send>,
410 >;
411
412 fn invoke_lambda_async(&self, function_arn: &str, payload: &str) -> Result<(), String> {
420 let _ = (function_arn, payload);
421 Err("this Lambda invoker does not support asynchronous invocation".to_string())
422 }
423
424 fn invoke_lambda_async_observed(
432 &self,
433 function_arn: &str,
434 payload: &str,
435 on_unrunnable: AsyncUnrunnableHook,
436 ) -> Result<(), String> {
437 drop(on_unrunnable);
438 self.invoke_lambda_async(function_arn, payload)
439 }
440
441 fn start_durable_async(&self, function_arn: &str, payload: &str) -> Option<Result<(), String>> {
447 let _ = (function_arn, payload);
448 None
449 }
450}
451
452pub trait KinesisDelivery: Send + Sync {
454 fn put_record(&self, stream_arn: &str, data: &str, partition_key: &str);
457}
458
459pub trait StepFunctionsDelivery: Send + Sync {
461 fn start_execution(&self, state_machine_arn: &str, input: &str);
464}
465
466pub trait SageMakerPipelineDelivery: Send + Sync {
471 fn start_pipeline_execution(&self, pipeline_arn: &str, parameters: &serde_json::Value);
472}
473
474pub trait FirehoseDelivery: Send + Sync {
480 fn put_record(&self, delivery_stream_arn: &str, data: &[u8]);
481}
482
483pub trait S3Delivery: Send + Sync {
487 fn put_object(
490 &self,
491 account_id: &str,
492 bucket: &str,
493 key: &str,
494 body: Vec<u8>,
495 content_type: Option<&str>,
496 ) -> Result<(), String>;
497
498 fn get_object(&self, account_id: &str, bucket: &str, key: &str) -> Result<Vec<u8>, String>;
503}
504
505pub trait SesSendEmailDispatcher: Send + Sync {
510 #[allow(clippy::too_many_arguments)]
511 fn send_email(
512 &self,
513 account_id: &str,
514 from: &str,
515 to: Vec<String>,
516 cc: Vec<String>,
517 bcc: Vec<String>,
518 subject: Option<&str>,
519 text_body: Option<&str>,
520 html_body: Option<&str>,
521 ) -> Result<(), String>;
522}
523
524pub trait EcsTaskRunner: Send + Sync {
528 fn run_task(
529 &self,
530 account_id: &str,
531 cluster: &str,
532 task_definition: &str,
533 launch_type: Option<&str>,
534 count: usize,
535 ) -> Result<(), String>;
536}
537
538pub trait Elbv2TargetRegistration: Send + Sync {
542 fn register_targets(
543 &self,
544 account_id: &str,
545 target_group_arn: &str,
546 targets: Vec<(String, Option<i64>)>,
547 );
548 fn deregister_targets(
549 &self,
550 account_id: &str,
551 target_group_arn: &str,
552 targets: Vec<(String, Option<i64>)>,
553 );
554}
555
556#[derive(Debug, Clone, PartialEq, Eq)]
558pub struct TaskEni {
559 pub eni_id: String,
560 pub private_ip: String,
561 pub mac_address: String,
562}
563
564pub trait Ec2NetworkLookup: Send + Sync {
569 fn create_task_eni(
572 &self,
573 account_id: &str,
574 region: &str,
575 subnet_id: &str,
576 group_ids: Vec<String>,
577 description: String,
578 ) -> Result<TaskEni, String>;
579 fn delete_task_eni(&self, account_id: &str, region: &str, eni_id: &str);
581}
582
583pub trait CloudwatchDelivery: Send + Sync {
587 #[allow(clippy::too_many_arguments)]
588 fn put_metric(
589 &self,
590 account_id: &str,
591 region: &str,
592 namespace: &str,
593 metric_name: &str,
594 value: f64,
595 unit: Option<&str>,
596 dimensions: std::collections::BTreeMap<String, String>,
597 timestamp_ms: i64,
598 );
599}
600
601pub trait CloudwatchLogsDelivery: Send + Sync {
606 fn put_log_events(
607 &self,
608 account_id: &str,
609 region: &str,
610 log_group_name: &str,
611 log_stream_name: &str,
612 events: &[(i64, String)],
613 );
614}
615
616pub trait EmailDispatcher: Send + Sync {
620 fn send_email(
621 &self,
622 account_id: &str,
623 from: &str,
624 to: &str,
625 subject: &str,
626 body_text: &str,
627 body_html: Option<&str>,
628 );
629}
630
631pub trait SmsDispatcher: Send + Sync {
634 fn send_sms(&self, account_id: &str, region: &str, phone_number: &str, message: &str);
637}
638
639#[derive(Debug, Clone, PartialEq, Eq)]
642pub enum KmsKeyUnusable {
643 NotFound,
645 Disabled,
647 InvalidState(String),
649 AccessDenied(String),
652}
653
654#[derive(Debug, Clone, Copy, PartialEq, Eq)]
666pub enum KmsCaller<'a> {
667 OnBehalfOf {
670 service_principal: &'a str,
671 caller_arn: Option<&'a str>,
675 },
676 Service {
680 service_principal: &'a str,
681 source_arn: &'a str,
682 source_account: &'a str,
683 via_service: Option<&'a str>,
687 },
688}
689
690impl<'a> KmsCaller<'a> {
691 pub fn via(service_principal: &'a str) -> Self {
694 KmsCaller::OnBehalfOf {
695 service_principal,
696 caller_arn: None,
697 }
698 }
699
700 pub fn via_for(service_principal: &'a str, caller_arn: Option<&'a str>) -> Self {
702 KmsCaller::OnBehalfOf {
703 service_principal,
704 caller_arn,
705 }
706 }
707
708 pub fn service_principal(&self) -> &'a str {
710 match self {
711 KmsCaller::OnBehalfOf {
712 service_principal, ..
713 }
714 | KmsCaller::Service {
715 service_principal, ..
716 } => service_principal,
717 }
718 }
719}
720
721#[derive(Debug, Clone, PartialEq, Eq)]
726pub struct KmsError {
727 pub kind: KmsErrorKind,
728 pub message: String,
729}
730
731#[derive(Debug, Clone, Copy, PartialEq, Eq)]
733pub enum KmsErrorKind {
734 NotFound,
736 Disabled,
738 InvalidState,
741 AccessDenied,
743 InvalidKeyUsage,
746 InvalidCiphertext,
749 Internal,
751}
752
753impl KmsErrorKind {
754 pub fn code(self) -> &'static str {
756 match self {
757 KmsErrorKind::NotFound => "NotFoundException",
758 KmsErrorKind::Disabled => "DisabledException",
759 KmsErrorKind::InvalidState => "KMSInvalidStateException",
760 KmsErrorKind::AccessDenied => "AccessDeniedException",
761 KmsErrorKind::InvalidKeyUsage => "InvalidKeyUsageException",
762 KmsErrorKind::InvalidCiphertext => "InvalidCiphertextException",
763 KmsErrorKind::Internal => "KMSInternalException",
764 }
765 }
766
767 pub fn status(self) -> u16 {
769 match self {
770 KmsErrorKind::Internal => 500,
771 _ => 400,
772 }
773 }
774
775 pub fn sqs_code(self) -> (u16, &'static str) {
780 match self {
781 KmsErrorKind::AccessDenied => (400, "KMS.AccessDeniedException"),
782 KmsErrorKind::Disabled => (400, "KMS.DisabledException"),
783 KmsErrorKind::InvalidKeyUsage => (400, "KMS.InvalidKeyUsageException"),
784 KmsErrorKind::InvalidState | KmsErrorKind::InvalidCiphertext => {
785 (400, "KMS.InvalidStateException")
786 }
787 KmsErrorKind::NotFound => (400, "KMS.NotFoundException"),
788 KmsErrorKind::Internal => (500, "KMS.InternalFailureException"),
789 }
790 }
791}
792
793impl KmsError {
794 pub fn new(kind: KmsErrorKind, message: impl Into<String>) -> Self {
795 Self {
796 kind,
797 message: message.into(),
798 }
799 }
800
801 pub fn not_found(account_id: &str, region: &str, key_ref: &str) -> Self {
805 Self::new(
806 KmsErrorKind::NotFound,
807 kms_not_found_message(account_id, region, key_ref),
808 )
809 }
810
811 pub fn not_configured() -> Self {
813 Self::new(KmsErrorKind::Internal, "KMS hook not configured")
814 }
815
816 pub fn relayed_message(&self) -> String {
822 format!(
823 "{} (Service: AWSKMS; Status Code: {}; Error Code: {}; Request ID: {}; Proxy: null)",
824 self.message,
825 self.kind.status(),
826 self.kind.code(),
827 uuid::Uuid::new_v4()
828 )
829 }
830}
831
832pub fn kms_not_found_message(account_id: &str, region: &str, key_ref: &str) -> String {
839 let parsed = fakecloud_aws::arn::arn_resource(key_ref, "kms").and_then(|rest| {
840 let (r, after) = rest.split_once(':')?;
841 let (a, res) = after.split_once(':')?;
842 Some((r, a, res))
843 });
844 let (ref_region, ref_account, resource) = parsed.unwrap_or((region, account_id, key_ref));
845 let ref_region = if ref_region.is_empty() {
846 region
847 } else {
848 ref_region
849 };
850 let account = if ref_account.is_empty() {
851 account_id
852 } else {
853 ref_account
854 };
855 if resource.starts_with("alias/") {
856 format!(
857 "Alias {} is not found.",
858 fakecloud_aws::arn::Arn::regional("kms", ref_region, account, resource)
859 )
860 } else {
861 let id = resource.strip_prefix("key/").unwrap_or(resource);
862 format!(
863 "Key '{}' does not exist",
864 fakecloud_aws::arn::Arn::regional("kms", ref_region, account, &format!("key/{id}"))
865 )
866 }
867}
868
869impl std::fmt::Display for KmsError {
870 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
871 write!(f, "{}: {}", self.kind.code(), self.message)
872 }
873}
874
875impl std::error::Error for KmsError {}
876
877pub trait KmsHook: Send + Sync {
888 fn encrypt(
891 &self,
892 account_id: &str,
893 region: &str,
894 key_id: &str,
895 plaintext: &[u8],
896 caller: KmsCaller<'_>,
897 encryption_context: std::collections::HashMap<String, String>,
898 ) -> Result<String, KmsError>;
899
900 #[allow(clippy::too_many_arguments)]
904 fn encrypt_with_operation(
905 &self,
906 _operation: &str,
907 account_id: &str,
908 region: &str,
909 key_id: &str,
910 plaintext: &[u8],
911 caller: KmsCaller<'_>,
912 encryption_context: std::collections::HashMap<String, String>,
913 ) -> Result<String, KmsError> {
914 self.encrypt(
915 account_id,
916 region,
917 key_id,
918 plaintext,
919 caller,
920 encryption_context,
921 )
922 }
923
924 fn decrypt(
927 &self,
928 account_id: &str,
929 ciphertext_b64: &str,
930 caller: KmsCaller<'_>,
931 encryption_context: std::collections::HashMap<String, String>,
932 ) -> Result<Vec<u8>, KmsError>;
933
934 fn decrypt_in_region(
939 &self,
940 account_id: &str,
941 _region: &str,
942 ciphertext_b64: &str,
943 caller: KmsCaller<'_>,
944 encryption_context: std::collections::HashMap<String, String>,
945 ) -> Result<Vec<u8>, KmsError> {
946 self.decrypt(account_id, ciphertext_b64, caller, encryption_context)
947 }
948
949 #[allow(clippy::too_many_arguments)]
955 fn authorize_key_use(
956 &self,
957 _account_id: &str,
958 _region: &str,
959 key_id: &str,
960 _caller: KmsCaller<'_>,
961 _actions: &[&str],
962 _encryption_context: &std::collections::HashMap<String, String>,
963 ) -> Result<String, KmsError> {
964 Ok(key_id.to_string())
965 }
966
967 fn resolve_key_arn(
971 &self,
972 _account_id: &str,
973 _region: &str,
974 _key_id: &str,
975 _service_principal: &str,
976 ) -> Result<String, String> {
977 Err("key resolution is not supported by this hook".to_string())
978 }
979
980 #[allow(clippy::too_many_arguments)]
988 fn check_service_key_use(
989 &self,
990 _account_id: &str,
991 _region: &str,
992 key_id: &str,
993 _service_principal: &str,
994 _source_arn: &str,
995 _actions: &[&str],
996 _encryption_context: &std::collections::HashMap<String, String>,
997 ) -> Result<String, KmsKeyUnusable> {
998 Ok(key_id.to_string())
999 }
1000
1001 fn is_ciphertext(&self, account_id: &str, stored: &str) -> bool;
1009
1010 fn aws_managed_key_arn(
1015 &self,
1016 _account_id: &str,
1017 _region: &str,
1018 _service: &str,
1019 _service_principal: &str,
1020 ) -> Result<String, String> {
1021 Err("AWS-managed key resolution is not supported by this hook".to_string())
1022 }
1023}
1024
1025pub fn aws_managed_kms_key_arn(
1034 hook: Option<&dyn KmsHook>,
1035 account_id: &str,
1036 region: &str,
1037 service: &str,
1038) -> Option<String> {
1039 let hook = hook?;
1040 let alias = format!("alias/aws/{service}");
1041 let principal = format!("{service}.amazonaws.com");
1042 match hook.aws_managed_key_arn(account_id, region, service, &principal) {
1043 Ok(arn) => Some(arn),
1044 Err(err) => {
1045 tracing::warn!(
1046 %err,
1047 account_id,
1048 region,
1049 alias = %alias,
1050 "could not resolve the AWS-managed KMS key; reporting no key"
1051 );
1052 None
1053 }
1054 }
1055}
1056
1057pub fn kms_key_or_aws_managed(
1061 hook: Option<&dyn KmsHook>,
1062 named: Option<&str>,
1063 account_id: &str,
1064 region: &str,
1065 service: &str,
1066) -> Option<String> {
1067 match named.filter(|k| !k.is_empty()) {
1068 Some(key) => Some(key.to_string()),
1069 None => aws_managed_kms_key_arn(hook, account_id, region, service),
1070 }
1071}
1072
1073pub fn kms_key_arn_or_aws_managed(
1077 hook: Option<&dyn KmsHook>,
1078 named: Option<&str>,
1079 account_id: &str,
1080 region: &str,
1081 service: &str,
1082) -> Option<String> {
1083 match named.filter(|k| !k.is_empty()) {
1084 Some(key) => Some(resolve_named_kms_key_arn(
1085 hook, key, account_id, region, service,
1086 )),
1087 None => aws_managed_kms_key_arn(hook, account_id, region, service),
1088 }
1089}
1090
1091pub fn resolve_named_kms_key_arn(
1097 hook: Option<&dyn KmsHook>,
1098 key: &str,
1099 account_id: &str,
1100 region: &str,
1101 service: &str,
1102) -> String {
1103 let Some(h) = hook else {
1104 return key.to_string();
1105 };
1106 if let Some(rest) = key.strip_prefix("arn:") {
1110 if rest.split(':').nth(2).is_some_and(|r| r != region) {
1111 return key.to_string();
1112 }
1113 }
1114 let resource = key.rsplit(':').next().unwrap_or(key);
1118 let owner = resource
1119 .strip_prefix("alias/aws/")
1120 .filter(|s| !s.is_empty())
1121 .unwrap_or(service);
1122 let principal = format!("{owner}.amazonaws.com");
1123 match h.resolve_key_arn(account_id, region, key, &principal) {
1124 Ok(arn) => arn,
1125 Err(err) => {
1126 tracing::debug!(%err, key, "KMS key not resolvable; reporting it as given");
1127 key.to_string()
1128 }
1129 }
1130}
1131
1132pub trait CognitoJwtVerifier: Send + Sync {
1140 fn verify_token(
1141 &self,
1142 account_id: &str,
1143 user_pool_arn: &str,
1144 token: &str,
1145 ) -> Result<serde_json::Value, String>;
1146}
1147
1148impl DeliveryBus {
1149 pub fn new() -> Self {
1150 Self {
1151 sqs_sender: None,
1152 sns_sender: None,
1153 eventbridge_sender: None,
1154 lambda_invoker: None,
1155 kinesis_sender: None,
1156 stepfunctions_starter: None,
1157 sagemaker_pipeline_starter: None,
1158 s3_writer: None,
1159 firehose_sender: None,
1160 ses_dispatcher: None,
1161 ecs_task_runner: None,
1162 elbv2_target_registration: None,
1163 ec2_network_lookup: None,
1164 cloudwatch_metrics: None,
1165 cloudwatch_logs: None,
1166 cognito_jwt_verifier: None,
1167 kms_hook: None,
1168 }
1169 }
1170
1171 pub fn with_cognito_jwt_verifier(mut self, verifier: Arc<dyn CognitoJwtVerifier>) -> Self {
1172 self.cognito_jwt_verifier = Some(verifier);
1173 self
1174 }
1175
1176 pub fn with_kms_hook(mut self, hook: Arc<dyn KmsHook>) -> Self {
1177 self.kms_hook = Some(hook);
1178 self
1179 }
1180
1181 pub fn kms_encrypt(
1184 &self,
1185 account_id: &str,
1186 region: &str,
1187 key_id: &str,
1188 plaintext: &[u8],
1189 caller: KmsCaller<'_>,
1190 encryption_context: std::collections::HashMap<String, String>,
1191 ) -> Result<String, KmsError> {
1192 match self.kms_hook {
1193 Some(ref h) => h.encrypt(
1194 account_id,
1195 region,
1196 key_id,
1197 plaintext,
1198 caller,
1199 encryption_context,
1200 ),
1201 None => Err(KmsError::not_configured()),
1202 }
1203 }
1204
1205 pub fn kms_decrypt(
1208 &self,
1209 account_id: &str,
1210 ciphertext_b64: &str,
1211 caller: KmsCaller<'_>,
1212 encryption_context: std::collections::HashMap<String, String>,
1213 ) -> Result<Vec<u8>, KmsError> {
1214 match self.kms_hook {
1215 Some(ref h) => h.decrypt(account_id, ciphertext_b64, caller, encryption_context),
1216 None => Err(KmsError::not_configured()),
1217 }
1218 }
1219
1220 pub fn verify_cognito_jwt(
1223 &self,
1224 account_id: &str,
1225 user_pool_arn: &str,
1226 token: &str,
1227 ) -> Result<serde_json::Value, String> {
1228 match self.cognito_jwt_verifier {
1229 Some(ref v) => v.verify_token(account_id, user_pool_arn, token),
1230 None => Err("Cognito JWT verifier not configured".to_string()),
1231 }
1232 }
1233
1234 pub fn with_cloudwatch_metrics(mut self, sender: Arc<dyn CloudwatchDelivery>) -> Self {
1235 self.cloudwatch_metrics = Some(sender);
1236 self
1237 }
1238
1239 #[allow(clippy::too_many_arguments)]
1243 pub fn put_cloudwatch_metric(
1244 &self,
1245 account_id: &str,
1246 region: &str,
1247 namespace: &str,
1248 metric_name: &str,
1249 value: f64,
1250 unit: Option<&str>,
1251 dimensions: std::collections::BTreeMap<String, String>,
1252 timestamp_ms: i64,
1253 ) {
1254 if let Some(ref sender) = self.cloudwatch_metrics {
1255 sender.put_metric(
1256 account_id,
1257 region,
1258 namespace,
1259 metric_name,
1260 value,
1261 unit,
1262 dimensions,
1263 timestamp_ms,
1264 );
1265 }
1266 }
1267
1268 pub fn with_cloudwatch_logs(mut self, sender: Arc<dyn CloudwatchLogsDelivery>) -> Self {
1269 self.cloudwatch_logs = Some(sender);
1270 self
1271 }
1272
1273 pub fn put_log_events(
1276 &self,
1277 account_id: &str,
1278 region: &str,
1279 log_group_name: &str,
1280 log_stream_name: &str,
1281 events: &[(i64, String)],
1282 ) {
1283 if let Some(ref sender) = self.cloudwatch_logs {
1284 sender.put_log_events(account_id, region, log_group_name, log_stream_name, events);
1285 }
1286 }
1287
1288 pub fn with_ses_dispatcher(mut self, dispatcher: Arc<dyn SesSendEmailDispatcher>) -> Self {
1289 self.ses_dispatcher = Some(dispatcher);
1290 self
1291 }
1292
1293 pub fn with_ecs_task_runner(mut self, runner: Arc<dyn EcsTaskRunner>) -> Self {
1294 self.ecs_task_runner = Some(runner);
1295 self
1296 }
1297
1298 pub fn with_elbv2_target_registration(mut self, reg: Arc<dyn Elbv2TargetRegistration>) -> Self {
1299 self.elbv2_target_registration = Some(reg);
1300 self
1301 }
1302
1303 pub fn with_ec2_network_lookup(mut self, lookup: Arc<dyn Ec2NetworkLookup>) -> Self {
1304 self.ec2_network_lookup = Some(lookup);
1305 self
1306 }
1307
1308 pub fn create_task_eni(
1311 &self,
1312 account_id: &str,
1313 region: &str,
1314 subnet_id: &str,
1315 group_ids: Vec<String>,
1316 description: String,
1317 ) -> Option<Result<TaskEni, String>> {
1318 self.ec2_network_lookup
1319 .as_ref()
1320 .map(|l| l.create_task_eni(account_id, region, subnet_id, group_ids, description))
1321 }
1322
1323 pub fn delete_task_eni(&self, account_id: &str, region: &str, eni_id: &str) {
1325 if let Some(l) = &self.ec2_network_lookup {
1326 l.delete_task_eni(account_id, region, eni_id);
1327 }
1328 }
1329
1330 pub fn register_elbv2_targets(
1333 &self,
1334 account_id: &str,
1335 target_group_arn: &str,
1336 targets: Vec<(String, Option<i64>)>,
1337 ) {
1338 if let Some(ref reg) = self.elbv2_target_registration {
1339 reg.register_targets(account_id, target_group_arn, targets);
1340 }
1341 }
1342
1343 pub fn deregister_elbv2_targets(
1346 &self,
1347 account_id: &str,
1348 target_group_arn: &str,
1349 targets: Vec<(String, Option<i64>)>,
1350 ) {
1351 if let Some(ref reg) = self.elbv2_target_registration {
1352 reg.deregister_targets(account_id, target_group_arn, targets);
1353 }
1354 }
1355
1356 #[allow(clippy::too_many_arguments)]
1359 pub fn send_ses_email(
1360 &self,
1361 account_id: &str,
1362 from: &str,
1363 to: Vec<String>,
1364 cc: Vec<String>,
1365 bcc: Vec<String>,
1366 subject: Option<&str>,
1367 text_body: Option<&str>,
1368 html_body: Option<&str>,
1369 ) -> Result<(), String> {
1370 match self.ses_dispatcher {
1371 Some(ref d) => {
1372 d.send_email(account_id, from, to, cc, bcc, subject, text_body, html_body)
1373 }
1374 None => Err("SES dispatcher not configured".to_string()),
1375 }
1376 }
1377
1378 pub fn run_ecs_task(
1381 &self,
1382 account_id: &str,
1383 cluster: &str,
1384 task_definition: &str,
1385 launch_type: Option<&str>,
1386 count: usize,
1387 ) -> Result<(), String> {
1388 match self.ecs_task_runner {
1389 Some(ref r) => r.run_task(account_id, cluster, task_definition, launch_type, count),
1390 None => Err("ECS task runner not configured".to_string()),
1391 }
1392 }
1393
1394 pub fn with_s3(mut self, sender: Arc<dyn S3Delivery>) -> Self {
1395 self.s3_writer = Some(sender);
1396 self
1397 }
1398
1399 pub fn with_firehose(mut self, sender: Arc<dyn FirehoseDelivery>) -> Self {
1400 self.firehose_sender = Some(sender);
1401 self
1402 }
1403
1404 pub fn put_record_to_firehose(&self, delivery_stream_arn: &str, data: &[u8]) {
1408 if let Some(ref sender) = self.firehose_sender {
1409 sender.put_record(delivery_stream_arn, data);
1410 }
1411 }
1412
1413 pub fn put_object_to_s3(
1416 &self,
1417 account_id: &str,
1418 bucket: &str,
1419 key: &str,
1420 body: Vec<u8>,
1421 content_type: Option<&str>,
1422 ) -> Result<(), String> {
1423 match self.s3_writer {
1424 Some(ref sender) => sender.put_object(account_id, bucket, key, body, content_type),
1425 None => Err("S3 writer not configured".to_string()),
1426 }
1427 }
1428
1429 pub fn get_object_from_s3(
1432 &self,
1433 account_id: &str,
1434 bucket: &str,
1435 key: &str,
1436 ) -> Result<Vec<u8>, String> {
1437 match self.s3_writer {
1438 Some(ref sender) => sender.get_object(account_id, bucket, key),
1439 None => Err("S3 client not configured".to_string()),
1440 }
1441 }
1442
1443 pub fn with_sqs(mut self, sender: Arc<dyn SqsDelivery>) -> Self {
1444 self.sqs_sender = Some(sender);
1445 self
1446 }
1447
1448 pub fn with_sns(mut self, sender: Arc<dyn SnsDelivery>) -> Self {
1449 self.sns_sender = Some(sender);
1450 self
1451 }
1452
1453 pub fn with_eventbridge(mut self, sender: Arc<dyn EventBridgeDelivery>) -> Self {
1454 self.eventbridge_sender = Some(sender);
1455 self
1456 }
1457
1458 pub fn with_lambda(mut self, invoker: Arc<dyn LambdaDelivery>) -> Self {
1459 self.lambda_invoker = Some(invoker);
1460 self
1461 }
1462
1463 pub fn with_kinesis(mut self, sender: Arc<dyn KinesisDelivery>) -> Self {
1464 self.kinesis_sender = Some(sender);
1465 self
1466 }
1467
1468 pub fn put_record_to_kinesis(&self, stream_arn: &str, data: &str, partition_key: &str) {
1471 if let Some(ref sender) = self.kinesis_sender {
1472 sender.put_record(stream_arn, data, partition_key);
1473 }
1474 }
1475
1476 pub fn with_sagemaker_pipeline(mut self, starter: Arc<dyn SageMakerPipelineDelivery>) -> Self {
1477 self.sagemaker_pipeline_starter = Some(starter);
1478 self
1479 }
1480
1481 pub fn start_sagemaker_pipeline(&self, pipeline_arn: &str, parameters: &serde_json::Value) {
1483 if let Some(ref starter) = self.sagemaker_pipeline_starter {
1484 starter.start_pipeline_execution(pipeline_arn, parameters);
1485 }
1486 }
1487
1488 pub fn with_stepfunctions(mut self, starter: Arc<dyn StepFunctionsDelivery>) -> Self {
1489 self.stepfunctions_starter = Some(starter);
1490 self
1491 }
1492
1493 pub fn sqs_queue_arn_for_url(&self, region: &str, queue_url: &str) -> Option<String> {
1496 self.sqs_sender
1497 .as_ref()?
1498 .queue_arn_for_url(region, queue_url)
1499 }
1500
1501 pub fn check_sqs_sender(
1504 &self,
1505 sender: DeliverySender<'_>,
1506 queue_arn: &str,
1507 ) -> Result<(), SqsDeliveryError> {
1508 match self.sqs_sender {
1509 Some(ref s) => s.check_sender(sender, queue_arn),
1510 None => Ok(()),
1511 }
1512 }
1513
1514 pub fn send_to_sqs(
1516 &self,
1517 from: DeliverySender<'_>,
1518 queue_arn: &str,
1519 message_body: &str,
1520 attributes: &HashMap<String, String>,
1521 ) {
1522 if let Some(ref sender) = self.sqs_sender {
1523 sender.deliver_to_queue(from, queue_arn, message_body, attributes);
1524 }
1525 }
1526
1527 pub fn send_to_sqs_with_attrs(
1529 &self,
1530 from: DeliverySender<'_>,
1531 queue_arn: &str,
1532 message_body: &str,
1533 message_attributes: &HashMap<String, SqsMessageAttribute>,
1534 message_group_id: Option<&str>,
1535 message_dedup_id: Option<&str>,
1536 ) {
1537 if let Some(ref sender) = self.sqs_sender {
1538 sender.deliver_to_queue_with_attrs(
1539 from,
1540 queue_arn,
1541 message_body,
1542 message_attributes,
1543 message_group_id,
1544 message_dedup_id,
1545 );
1546 }
1547 }
1548
1549 pub fn try_send_to_sqs_with_attrs(
1554 &self,
1555 from: DeliverySender<'_>,
1556 queue_arn: &str,
1557 message_body: &str,
1558 message_attributes: &HashMap<String, SqsMessageAttribute>,
1559 message_group_id: Option<&str>,
1560 message_dedup_id: Option<&str>,
1561 ) -> Result<(), SqsDeliveryError> {
1562 match self.sqs_sender {
1563 Some(ref sender) => sender.try_deliver_to_queue_with_attrs(
1564 from,
1565 queue_arn,
1566 message_body,
1567 message_attributes,
1568 message_group_id,
1569 message_dedup_id,
1570 ),
1571 None => Err(SqsDeliveryError::QueueNotFound(queue_arn.to_string())),
1572 }
1573 }
1574
1575 pub fn publish_to_sns(&self, topic_arn: &str, message: &str, subject: Option<&str>) {
1577 if let Some(ref sender) = self.sns_sender {
1578 sender.publish_to_topic(topic_arn, message, subject);
1579 }
1580 }
1581
1582 pub fn publish_to_sns_with_attributes(
1584 &self,
1585 topic_arn: &str,
1586 message: &str,
1587 subject: Option<&str>,
1588 attributes: &HashMap<String, SqsMessageAttribute>,
1589 ) {
1590 if let Some(ref sender) = self.sns_sender {
1591 sender.publish_to_topic_with_attributes(topic_arn, message, subject, attributes);
1592 }
1593 }
1594
1595 pub fn put_event_to_eventbridge(
1599 &self,
1600 event: &CrossServiceEvent<'_>,
1601 ) -> Result<Option<String>, EventBridgeDeliveryError> {
1602 match self.eventbridge_sender {
1603 Some(ref sender) => sender.put_event(event).map(Some),
1604 None => Ok(None),
1605 }
1606 }
1607
1608 pub async fn invoke_lambda(
1610 &self,
1611 function_arn: &str,
1612 payload: &str,
1613 ) -> Option<Result<LambdaInvocationOutput, String>> {
1614 if let Some(ref invoker) = self.lambda_invoker {
1615 Some(invoker.invoke_lambda(function_arn, payload).await)
1616 } else {
1617 None
1618 }
1619 }
1620
1621 pub fn invoke_lambda_async(
1625 &self,
1626 function_arn: &str,
1627 payload: &str,
1628 ) -> Option<Result<(), String>> {
1629 self.lambda_invoker
1630 .as_ref()
1631 .map(|invoker| invoker.invoke_lambda_async(function_arn, payload))
1632 }
1633
1634 pub fn invoke_lambda_async_observed(
1637 &self,
1638 function_arn: &str,
1639 payload: &str,
1640 on_unrunnable: AsyncUnrunnableHook,
1641 ) -> Option<Result<(), String>> {
1642 self.lambda_invoker.as_ref().map(|invoker| {
1643 invoker.invoke_lambda_async_observed(function_arn, payload, on_unrunnable)
1644 })
1645 }
1646
1647 pub fn start_durable_lambda_async(
1650 &self,
1651 function_arn: &str,
1652 payload: &str,
1653 ) -> Option<Result<(), String>> {
1654 self.lambda_invoker
1655 .as_ref()?
1656 .start_durable_async(function_arn, payload)
1657 }
1658
1659 pub fn send_to_kinesis(&self, stream_arn: &str, data: &str, partition_key: &str) {
1661 if let Some(ref sender) = self.kinesis_sender {
1662 sender.put_record(stream_arn, data, partition_key);
1663 }
1664 }
1665
1666 pub fn start_stepfunctions_execution(&self, state_machine_arn: &str, input: &str) {
1668 if let Some(ref starter) = self.stepfunctions_starter {
1669 starter.start_execution(state_machine_arn, input);
1670 }
1671 }
1672}
1673
1674impl Default for DeliveryBus {
1675 fn default() -> Self {
1676 Self::new()
1677 }
1678}
1679
1680#[cfg(test)]
1681mod tests {
1682 use super::*;
1683 use std::sync::atomic::{AtomicUsize, Ordering};
1684 use std::sync::Arc;
1685
1686 struct MockSqs {
1688 call_count: AtomicUsize,
1689 }
1690 impl SqsDelivery for MockSqs {
1691 fn deliver_to_queue(
1692 &self,
1693 _sender: DeliverySender<'_>,
1694 _queue_arn: &str,
1695 _message_body: &str,
1696 _attributes: &HashMap<String, String>,
1697 ) {
1698 self.call_count.fetch_add(1, Ordering::SeqCst);
1699 }
1700 }
1701
1702 struct MockSns {
1703 call_count: AtomicUsize,
1704 }
1705 impl SnsDelivery for MockSns {
1706 fn publish_to_topic(&self, _topic_arn: &str, _message: &str, _subject: Option<&str>) {
1707 self.call_count.fetch_add(1, Ordering::SeqCst);
1708 }
1709 }
1710
1711 struct MockEventBridge {
1712 call_count: AtomicUsize,
1713 }
1714 impl EventBridgeDelivery for MockEventBridge {
1715 fn put_event(
1716 &self,
1717 _event: &CrossServiceEvent<'_>,
1718 ) -> Result<String, EventBridgeDeliveryError> {
1719 self.call_count.fetch_add(1, Ordering::SeqCst);
1720 Ok("id".to_string())
1721 }
1722 }
1723
1724 fn test_event<'a>(source: &'a str, bus: &'a str) -> CrossServiceEvent<'a> {
1725 CrossServiceEvent {
1726 source,
1727 detail_type: "type",
1728 detail: "{}",
1729 event_bus: bus,
1730 account_id: "111111111111",
1731 region: "eu-west-1",
1732 resources: &[],
1733 principal_arn: None,
1734 }
1735 }
1736
1737 struct MockKinesis {
1738 call_count: AtomicUsize,
1739 }
1740 impl KinesisDelivery for MockKinesis {
1741 fn put_record(&self, _stream_arn: &str, _data: &str, _partition_key: &str) {
1742 self.call_count.fetch_add(1, Ordering::SeqCst);
1743 }
1744 }
1745
1746 struct MockStepFunctions {
1747 call_count: AtomicUsize,
1748 }
1749 impl StepFunctionsDelivery for MockStepFunctions {
1750 fn start_execution(&self, _state_machine_arn: &str, _input: &str) {
1751 self.call_count.fetch_add(1, Ordering::SeqCst);
1752 }
1753 }
1754
1755 #[test]
1756 fn delivery_bus_new_has_no_senders() {
1757 let bus = DeliveryBus::new();
1758 bus.send_to_sqs(
1760 DeliverySender::Identity { arn: None },
1761 "arn:queue",
1762 "body",
1763 &HashMap::new(),
1764 );
1765 bus.publish_to_sns("arn:topic", "msg", None);
1766 let _ = bus.put_event_to_eventbridge(&test_event("src", "default"));
1767 bus.send_to_kinesis("arn:stream", "data", "pk");
1768 bus.start_stepfunctions_execution("arn:sfn", "{}");
1769 }
1771
1772 #[test]
1773 fn delivery_bus_default_is_same_as_new() {
1774 let bus = DeliveryBus::default();
1775 bus.send_to_sqs(
1776 DeliverySender::Identity { arn: None },
1777 "arn:q",
1778 "b",
1779 &HashMap::new(),
1780 );
1781 }
1782
1783 #[test]
1784 fn send_to_sqs_calls_sender() {
1785 let mock = Arc::new(MockSqs {
1786 call_count: AtomicUsize::new(0),
1787 });
1788 let bus = DeliveryBus::new().with_sqs(mock.clone());
1789
1790 bus.send_to_sqs(
1791 DeliverySender::Identity { arn: None },
1792 "arn:queue",
1793 "msg",
1794 &HashMap::new(),
1795 );
1796 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1797
1798 bus.send_to_sqs(
1799 DeliverySender::Identity { arn: None },
1800 "arn:queue2",
1801 "msg2",
1802 &HashMap::new(),
1803 );
1804 assert_eq!(mock.call_count.load(Ordering::SeqCst), 2);
1805 }
1806
1807 #[test]
1808 fn send_to_sqs_with_attrs_calls_sender() {
1809 let mock = Arc::new(MockSqs {
1810 call_count: AtomicUsize::new(0),
1811 });
1812 let bus = DeliveryBus::new().with_sqs(mock.clone());
1813
1814 let mut attrs = HashMap::new();
1815 attrs.insert(
1816 "key".to_string(),
1817 SqsMessageAttribute {
1818 data_type: "String".to_string(),
1819 string_value: Some("val".to_string()),
1820 binary_value: None,
1821 },
1822 );
1823 bus.send_to_sqs_with_attrs(
1824 DeliverySender::Identity { arn: None },
1825 "arn:q",
1826 "body",
1827 &attrs,
1828 Some("group"),
1829 Some("dedup"),
1830 );
1831 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1832 }
1833
1834 #[test]
1835 fn publish_to_sns_calls_sender() {
1836 let mock = Arc::new(MockSns {
1837 call_count: AtomicUsize::new(0),
1838 });
1839 let bus = DeliveryBus::new().with_sns(mock.clone());
1840
1841 bus.publish_to_sns("arn:topic", "message", Some("subject"));
1842 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1843 }
1844
1845 #[test]
1846 fn put_event_to_eventbridge_calls_sender() {
1847 let mock = Arc::new(MockEventBridge {
1848 call_count: AtomicUsize::new(0),
1849 });
1850 let bus = DeliveryBus::new().with_eventbridge(mock.clone());
1851
1852 let _ = bus.put_event_to_eventbridge(&test_event("aws.s3", "default"));
1853 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1854 }
1855
1856 #[test]
1857 fn send_to_kinesis_calls_sender() {
1858 let mock = Arc::new(MockKinesis {
1859 call_count: AtomicUsize::new(0),
1860 });
1861 let bus = DeliveryBus::new().with_kinesis(mock.clone());
1862
1863 bus.send_to_kinesis("arn:stream", "data", "partition-key");
1864 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1865 }
1866
1867 #[test]
1868 fn start_stepfunctions_calls_sender() {
1869 let mock = Arc::new(MockStepFunctions {
1870 call_count: AtomicUsize::new(0),
1871 });
1872 let bus = DeliveryBus::new().with_stepfunctions(mock.clone());
1873
1874 bus.start_stepfunctions_execution("arn:sfn:machine", r#"{"key":"val"}"#);
1875 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1876 }
1877
1878 #[test]
1879 fn builder_chaining_works() {
1880 let sqs = Arc::new(MockSqs {
1881 call_count: AtomicUsize::new(0),
1882 });
1883 let sns = Arc::new(MockSns {
1884 call_count: AtomicUsize::new(0),
1885 });
1886 let eb = Arc::new(MockEventBridge {
1887 call_count: AtomicUsize::new(0),
1888 });
1889 let kin = Arc::new(MockKinesis {
1890 call_count: AtomicUsize::new(0),
1891 });
1892 let sfn = Arc::new(MockStepFunctions {
1893 call_count: AtomicUsize::new(0),
1894 });
1895
1896 let bus = DeliveryBus::new()
1897 .with_sqs(sqs.clone())
1898 .with_sns(sns.clone())
1899 .with_eventbridge(eb.clone())
1900 .with_kinesis(kin.clone())
1901 .with_stepfunctions(sfn.clone());
1902
1903 bus.send_to_sqs(
1904 DeliverySender::Identity { arn: None },
1905 "q",
1906 "m",
1907 &HashMap::new(),
1908 );
1909 bus.publish_to_sns("t", "m", None);
1910 let _ = bus.put_event_to_eventbridge(&test_event("s", "b"));
1911 bus.send_to_kinesis("s", "d", "k");
1912 bus.start_stepfunctions_execution("sm", "{}");
1913
1914 assert_eq!(sqs.call_count.load(Ordering::SeqCst), 1);
1915 assert_eq!(sns.call_count.load(Ordering::SeqCst), 1);
1916 assert_eq!(eb.call_count.load(Ordering::SeqCst), 1);
1917 assert_eq!(kin.call_count.load(Ordering::SeqCst), 1);
1918 assert_eq!(sfn.call_count.load(Ordering::SeqCst), 1);
1919 }
1920
1921 #[test]
1922 fn invoke_lambda_async_returns_none_without_invoker() {
1923 let bus = DeliveryBus::new();
1924 assert!(bus.invoke_lambda_async("arn:lambda", "{}").is_none());
1925 }
1926
1927 #[test]
1928 fn invocation_output_reads_error_fields_only_for_function_errors() {
1929 let shaped = br#"{"errorType":"X","errorMessage":"m"}"#;
1930 let ok = LambdaInvocationOutput::success(shaped.to_vec());
1931 assert!(!ok.is_function_error());
1932 assert_eq!(ok.error_type(), None);
1933 assert_eq!(ok.error_message(), None);
1934
1935 let failed = LambdaInvocationOutput::unhandled(shaped.to_vec());
1936 assert_eq!(failed.function_error.as_deref(), Some("Unhandled"));
1937 assert_eq!(failed.error_type().as_deref(), Some("X"));
1938 assert_eq!(failed.error_message().as_deref(), Some("m"));
1939
1940 let untyped =
1941 LambdaInvocationOutput::unhandled(br#"{"errorMessage":"timed out"}"#.to_vec());
1942 assert_eq!(untyped.error_type(), None);
1943 assert_eq!(untyped.error_message().as_deref(), Some("timed out"));
1944 }
1945
1946 #[tokio::test]
1947 async fn invoke_lambda_returns_none_without_invoker() {
1948 let bus = DeliveryBus::new();
1949 let result = bus.invoke_lambda("arn:lambda", "{}").await;
1950 assert!(result.is_none());
1951 }
1952}