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 cloudwatch_metrics: Option<Arc<dyn CloudwatchDelivery>>,
37 cloudwatch_logs: Option<Arc<dyn CloudwatchLogsDelivery>>,
40 cognito_jwt_verifier: Option<Arc<dyn CognitoJwtVerifier>>,
44 kms_hook: Option<Arc<dyn KmsHook>>,
47}
48
49#[derive(Debug, Clone)]
51pub struct SqsMessageAttribute {
52 pub data_type: String,
53 pub string_value: Option<String>,
54 pub binary_value: Option<String>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum SqsDeliveryError {
63 QueueNotFound(String),
65 InvalidArn(String),
67 InvalidParameter(String),
72}
73
74impl std::fmt::Display for SqsDeliveryError {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 Self::QueueNotFound(arn) => write!(f, "queue not found: {arn}"),
78 Self::InvalidArn(arn) => write!(f, "invalid queue ARN: {arn}"),
79 Self::InvalidParameter(msg) => write!(f, "invalid parameter: {msg}"),
80 }
81 }
82}
83
84impl std::error::Error for SqsDeliveryError {}
85
86pub trait SqsDelivery: Send + Sync {
88 fn deliver_to_queue(
89 &self,
90 queue_arn: &str,
91 message_body: &str,
92 attributes: &HashMap<String, String>,
93 );
94
95 fn deliver_to_queue_with_attrs(
97 &self,
98 queue_arn: &str,
99 message_body: &str,
100 message_attributes: &HashMap<String, SqsMessageAttribute>,
101 message_group_id: Option<&str>,
102 message_dedup_id: Option<&str>,
103 ) {
104 let _ = (message_attributes, message_group_id, message_dedup_id);
106 self.deliver_to_queue(queue_arn, message_body, &HashMap::new());
107 }
108
109 fn try_deliver_to_queue_with_attrs(
115 &self,
116 queue_arn: &str,
117 message_body: &str,
118 message_attributes: &HashMap<String, SqsMessageAttribute>,
119 message_group_id: Option<&str>,
120 message_dedup_id: Option<&str>,
121 ) -> Result<(), SqsDeliveryError> {
122 self.deliver_to_queue_with_attrs(
123 queue_arn,
124 message_body,
125 message_attributes,
126 message_group_id,
127 message_dedup_id,
128 );
129 Ok(())
130 }
131}
132
133pub trait SnsDelivery: Send + Sync {
135 fn publish_to_topic(&self, topic_arn: &str, message: &str, subject: Option<&str>);
136
137 fn publish_to_topic_fifo(
141 &self,
142 topic_arn: &str,
143 message: &str,
144 subject: Option<&str>,
145 _message_group_id: Option<&str>,
146 _message_dedup_id: Option<&str>,
147 ) {
148 self.publish_to_topic(topic_arn, message, subject);
149 }
150}
151
152pub trait EventBridgeDelivery: Send + Sync {
154 fn put_event(&self, source: &str, detail_type: &str, detail: &str, event_bus_name: &str);
157
158 fn put_event_to_account(
165 &self,
166 source: &str,
167 detail_type: &str,
168 detail: &str,
169 event_bus_name: &str,
170 _target_account_id: &str,
171 ) {
172 self.put_event(source, detail_type, detail, event_bus_name);
173 }
174}
175
176pub trait LambdaDelivery: Send + Sync {
178 fn invoke_lambda(
181 &self,
182 function_arn: &str,
183 payload: &str,
184 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<u8>, String>> + Send>>;
185}
186
187pub trait KinesisDelivery: Send + Sync {
189 fn put_record(&self, stream_arn: &str, data: &str, partition_key: &str);
192}
193
194pub trait StepFunctionsDelivery: Send + Sync {
196 fn start_execution(&self, state_machine_arn: &str, input: &str);
199}
200
201pub trait SageMakerPipelineDelivery: Send + Sync {
206 fn start_pipeline_execution(&self, pipeline_arn: &str, parameters: &serde_json::Value);
207}
208
209pub trait FirehoseDelivery: Send + Sync {
215 fn put_record(&self, delivery_stream_arn: &str, data: &[u8]);
216}
217
218pub trait S3Delivery: Send + Sync {
222 fn put_object(
225 &self,
226 account_id: &str,
227 bucket: &str,
228 key: &str,
229 body: Vec<u8>,
230 content_type: Option<&str>,
231 ) -> Result<(), String>;
232
233 fn get_object(&self, account_id: &str, bucket: &str, key: &str) -> Result<Vec<u8>, String>;
238}
239
240pub trait SesSendEmailDispatcher: Send + Sync {
245 #[allow(clippy::too_many_arguments)]
246 fn send_email(
247 &self,
248 account_id: &str,
249 from: &str,
250 to: Vec<String>,
251 cc: Vec<String>,
252 bcc: Vec<String>,
253 subject: Option<&str>,
254 text_body: Option<&str>,
255 html_body: Option<&str>,
256 ) -> Result<(), String>;
257}
258
259pub trait EcsTaskRunner: Send + Sync {
263 fn run_task(
264 &self,
265 account_id: &str,
266 cluster: &str,
267 task_definition: &str,
268 launch_type: Option<&str>,
269 count: usize,
270 ) -> Result<(), String>;
271}
272
273pub trait Elbv2TargetRegistration: Send + Sync {
277 fn register_targets(
278 &self,
279 account_id: &str,
280 target_group_arn: &str,
281 targets: Vec<(String, Option<i64>)>,
282 );
283 fn deregister_targets(
284 &self,
285 account_id: &str,
286 target_group_arn: &str,
287 targets: Vec<(String, Option<i64>)>,
288 );
289}
290
291pub trait CloudwatchDelivery: Send + Sync {
295 #[allow(clippy::too_many_arguments)]
296 fn put_metric(
297 &self,
298 account_id: &str,
299 region: &str,
300 namespace: &str,
301 metric_name: &str,
302 value: f64,
303 unit: Option<&str>,
304 dimensions: std::collections::BTreeMap<String, String>,
305 timestamp_ms: i64,
306 );
307}
308
309pub trait CloudwatchLogsDelivery: Send + Sync {
314 fn put_log_events(
315 &self,
316 account_id: &str,
317 log_group_name: &str,
318 log_stream_name: &str,
319 events: &[(i64, String)],
320 );
321}
322
323pub trait EmailDispatcher: Send + Sync {
327 fn send_email(
328 &self,
329 account_id: &str,
330 from: &str,
331 to: &str,
332 subject: &str,
333 body_text: &str,
334 body_html: Option<&str>,
335 );
336}
337
338pub trait SmsDispatcher: Send + Sync {
341 fn send_sms(&self, account_id: &str, phone_number: &str, message: &str);
342}
343
344pub trait KmsHook: Send + Sync {
354 fn encrypt(
355 &self,
356 account_id: &str,
357 region: &str,
358 key_id: &str,
359 plaintext: &[u8],
360 service_principal: &str,
361 encryption_context: std::collections::HashMap<String, String>,
362 ) -> Result<String, String>;
363
364 fn decrypt(
365 &self,
366 account_id: &str,
367 ciphertext_b64: &str,
368 service_principal: &str,
369 encryption_context: std::collections::HashMap<String, String>,
370 ) -> Result<Vec<u8>, String>;
371}
372
373pub trait CognitoJwtVerifier: Send + Sync {
381 fn verify_token(
382 &self,
383 account_id: &str,
384 user_pool_arn: &str,
385 token: &str,
386 ) -> Result<serde_json::Value, String>;
387}
388
389impl DeliveryBus {
390 pub fn new() -> Self {
391 Self {
392 sqs_sender: None,
393 sns_sender: None,
394 eventbridge_sender: None,
395 lambda_invoker: None,
396 kinesis_sender: None,
397 stepfunctions_starter: None,
398 sagemaker_pipeline_starter: None,
399 s3_writer: None,
400 firehose_sender: None,
401 ses_dispatcher: None,
402 ecs_task_runner: None,
403 elbv2_target_registration: None,
404 cloudwatch_metrics: None,
405 cloudwatch_logs: None,
406 cognito_jwt_verifier: None,
407 kms_hook: None,
408 }
409 }
410
411 pub fn with_cognito_jwt_verifier(mut self, verifier: Arc<dyn CognitoJwtVerifier>) -> Self {
412 self.cognito_jwt_verifier = Some(verifier);
413 self
414 }
415
416 pub fn with_kms_hook(mut self, hook: Arc<dyn KmsHook>) -> Self {
417 self.kms_hook = Some(hook);
418 self
419 }
420
421 pub fn kms_encrypt(
424 &self,
425 account_id: &str,
426 region: &str,
427 key_id: &str,
428 plaintext: &[u8],
429 service_principal: &str,
430 encryption_context: std::collections::HashMap<String, String>,
431 ) -> Result<String, String> {
432 match self.kms_hook {
433 Some(ref h) => h.encrypt(
434 account_id,
435 region,
436 key_id,
437 plaintext,
438 service_principal,
439 encryption_context,
440 ),
441 None => Err("KMS hook not configured".to_string()),
442 }
443 }
444
445 pub fn kms_decrypt(
448 &self,
449 account_id: &str,
450 ciphertext_b64: &str,
451 service_principal: &str,
452 encryption_context: std::collections::HashMap<String, String>,
453 ) -> Result<Vec<u8>, String> {
454 match self.kms_hook {
455 Some(ref h) => h.decrypt(
456 account_id,
457 ciphertext_b64,
458 service_principal,
459 encryption_context,
460 ),
461 None => Err("KMS hook not configured".to_string()),
462 }
463 }
464
465 pub fn verify_cognito_jwt(
468 &self,
469 account_id: &str,
470 user_pool_arn: &str,
471 token: &str,
472 ) -> Result<serde_json::Value, String> {
473 match self.cognito_jwt_verifier {
474 Some(ref v) => v.verify_token(account_id, user_pool_arn, token),
475 None => Err("Cognito JWT verifier not configured".to_string()),
476 }
477 }
478
479 pub fn with_cloudwatch_metrics(mut self, sender: Arc<dyn CloudwatchDelivery>) -> Self {
480 self.cloudwatch_metrics = Some(sender);
481 self
482 }
483
484 #[allow(clippy::too_many_arguments)]
488 pub fn put_cloudwatch_metric(
489 &self,
490 account_id: &str,
491 region: &str,
492 namespace: &str,
493 metric_name: &str,
494 value: f64,
495 unit: Option<&str>,
496 dimensions: std::collections::BTreeMap<String, String>,
497 timestamp_ms: i64,
498 ) {
499 if let Some(ref sender) = self.cloudwatch_metrics {
500 sender.put_metric(
501 account_id,
502 region,
503 namespace,
504 metric_name,
505 value,
506 unit,
507 dimensions,
508 timestamp_ms,
509 );
510 }
511 }
512
513 pub fn with_cloudwatch_logs(mut self, sender: Arc<dyn CloudwatchLogsDelivery>) -> Self {
514 self.cloudwatch_logs = Some(sender);
515 self
516 }
517
518 pub fn put_log_events(
521 &self,
522 account_id: &str,
523 log_group_name: &str,
524 log_stream_name: &str,
525 events: &[(i64, String)],
526 ) {
527 if let Some(ref sender) = self.cloudwatch_logs {
528 sender.put_log_events(account_id, log_group_name, log_stream_name, events);
529 }
530 }
531
532 pub fn with_ses_dispatcher(mut self, dispatcher: Arc<dyn SesSendEmailDispatcher>) -> Self {
533 self.ses_dispatcher = Some(dispatcher);
534 self
535 }
536
537 pub fn with_ecs_task_runner(mut self, runner: Arc<dyn EcsTaskRunner>) -> Self {
538 self.ecs_task_runner = Some(runner);
539 self
540 }
541
542 pub fn with_elbv2_target_registration(mut self, reg: Arc<dyn Elbv2TargetRegistration>) -> Self {
543 self.elbv2_target_registration = Some(reg);
544 self
545 }
546
547 pub fn register_elbv2_targets(
550 &self,
551 account_id: &str,
552 target_group_arn: &str,
553 targets: Vec<(String, Option<i64>)>,
554 ) {
555 if let Some(ref reg) = self.elbv2_target_registration {
556 reg.register_targets(account_id, target_group_arn, targets);
557 }
558 }
559
560 pub fn deregister_elbv2_targets(
563 &self,
564 account_id: &str,
565 target_group_arn: &str,
566 targets: Vec<(String, Option<i64>)>,
567 ) {
568 if let Some(ref reg) = self.elbv2_target_registration {
569 reg.deregister_targets(account_id, target_group_arn, targets);
570 }
571 }
572
573 #[allow(clippy::too_many_arguments)]
576 pub fn send_ses_email(
577 &self,
578 account_id: &str,
579 from: &str,
580 to: Vec<String>,
581 cc: Vec<String>,
582 bcc: Vec<String>,
583 subject: Option<&str>,
584 text_body: Option<&str>,
585 html_body: Option<&str>,
586 ) -> Result<(), String> {
587 match self.ses_dispatcher {
588 Some(ref d) => {
589 d.send_email(account_id, from, to, cc, bcc, subject, text_body, html_body)
590 }
591 None => Err("SES dispatcher not configured".to_string()),
592 }
593 }
594
595 pub fn run_ecs_task(
598 &self,
599 account_id: &str,
600 cluster: &str,
601 task_definition: &str,
602 launch_type: Option<&str>,
603 count: usize,
604 ) -> Result<(), String> {
605 match self.ecs_task_runner {
606 Some(ref r) => r.run_task(account_id, cluster, task_definition, launch_type, count),
607 None => Err("ECS task runner not configured".to_string()),
608 }
609 }
610
611 pub fn with_s3(mut self, sender: Arc<dyn S3Delivery>) -> Self {
612 self.s3_writer = Some(sender);
613 self
614 }
615
616 pub fn with_firehose(mut self, sender: Arc<dyn FirehoseDelivery>) -> Self {
617 self.firehose_sender = Some(sender);
618 self
619 }
620
621 pub fn put_record_to_firehose(&self, delivery_stream_arn: &str, data: &[u8]) {
625 if let Some(ref sender) = self.firehose_sender {
626 sender.put_record(delivery_stream_arn, data);
627 }
628 }
629
630 pub fn put_object_to_s3(
633 &self,
634 account_id: &str,
635 bucket: &str,
636 key: &str,
637 body: Vec<u8>,
638 content_type: Option<&str>,
639 ) -> Result<(), String> {
640 match self.s3_writer {
641 Some(ref sender) => sender.put_object(account_id, bucket, key, body, content_type),
642 None => Err("S3 writer not configured".to_string()),
643 }
644 }
645
646 pub fn get_object_from_s3(
649 &self,
650 account_id: &str,
651 bucket: &str,
652 key: &str,
653 ) -> Result<Vec<u8>, String> {
654 match self.s3_writer {
655 Some(ref sender) => sender.get_object(account_id, bucket, key),
656 None => Err("S3 client not configured".to_string()),
657 }
658 }
659
660 pub fn with_sqs(mut self, sender: Arc<dyn SqsDelivery>) -> Self {
661 self.sqs_sender = Some(sender);
662 self
663 }
664
665 pub fn with_sns(mut self, sender: Arc<dyn SnsDelivery>) -> Self {
666 self.sns_sender = Some(sender);
667 self
668 }
669
670 pub fn with_eventbridge(mut self, sender: Arc<dyn EventBridgeDelivery>) -> Self {
671 self.eventbridge_sender = Some(sender);
672 self
673 }
674
675 pub fn with_lambda(mut self, invoker: Arc<dyn LambdaDelivery>) -> Self {
676 self.lambda_invoker = Some(invoker);
677 self
678 }
679
680 pub fn with_kinesis(mut self, sender: Arc<dyn KinesisDelivery>) -> Self {
681 self.kinesis_sender = Some(sender);
682 self
683 }
684
685 pub fn put_record_to_kinesis(&self, stream_arn: &str, data: &str, partition_key: &str) {
688 if let Some(ref sender) = self.kinesis_sender {
689 sender.put_record(stream_arn, data, partition_key);
690 }
691 }
692
693 pub fn with_sagemaker_pipeline(mut self, starter: Arc<dyn SageMakerPipelineDelivery>) -> Self {
694 self.sagemaker_pipeline_starter = Some(starter);
695 self
696 }
697
698 pub fn start_sagemaker_pipeline(&self, pipeline_arn: &str, parameters: &serde_json::Value) {
700 if let Some(ref starter) = self.sagemaker_pipeline_starter {
701 starter.start_pipeline_execution(pipeline_arn, parameters);
702 }
703 }
704
705 pub fn with_stepfunctions(mut self, starter: Arc<dyn StepFunctionsDelivery>) -> Self {
706 self.stepfunctions_starter = Some(starter);
707 self
708 }
709
710 pub fn send_to_sqs(
712 &self,
713 queue_arn: &str,
714 message_body: &str,
715 attributes: &HashMap<String, String>,
716 ) {
717 if let Some(ref sender) = self.sqs_sender {
718 sender.deliver_to_queue(queue_arn, message_body, attributes);
719 }
720 }
721
722 pub fn send_to_sqs_with_attrs(
724 &self,
725 queue_arn: &str,
726 message_body: &str,
727 message_attributes: &HashMap<String, SqsMessageAttribute>,
728 message_group_id: Option<&str>,
729 message_dedup_id: Option<&str>,
730 ) {
731 if let Some(ref sender) = self.sqs_sender {
732 sender.deliver_to_queue_with_attrs(
733 queue_arn,
734 message_body,
735 message_attributes,
736 message_group_id,
737 message_dedup_id,
738 );
739 }
740 }
741
742 pub fn try_send_to_sqs_with_attrs(
747 &self,
748 queue_arn: &str,
749 message_body: &str,
750 message_attributes: &HashMap<String, SqsMessageAttribute>,
751 message_group_id: Option<&str>,
752 message_dedup_id: Option<&str>,
753 ) -> Result<(), SqsDeliveryError> {
754 match self.sqs_sender {
755 Some(ref sender) => sender.try_deliver_to_queue_with_attrs(
756 queue_arn,
757 message_body,
758 message_attributes,
759 message_group_id,
760 message_dedup_id,
761 ),
762 None => Err(SqsDeliveryError::QueueNotFound(queue_arn.to_string())),
763 }
764 }
765
766 pub fn publish_to_sns(&self, topic_arn: &str, message: &str, subject: Option<&str>) {
768 if let Some(ref sender) = self.sns_sender {
769 sender.publish_to_topic(topic_arn, message, subject);
770 }
771 }
772
773 pub fn put_event_to_eventbridge(
775 &self,
776 source: &str,
777 detail_type: &str,
778 detail: &str,
779 event_bus_name: &str,
780 ) {
781 if let Some(ref sender) = self.eventbridge_sender {
782 sender.put_event(source, detail_type, detail, event_bus_name);
783 }
784 }
785
786 pub fn put_event_to_eventbridge_for_account(
789 &self,
790 source: &str,
791 detail_type: &str,
792 detail: &str,
793 event_bus_name: &str,
794 target_account_id: &str,
795 ) {
796 if let Some(ref sender) = self.eventbridge_sender {
797 sender.put_event_to_account(
798 source,
799 detail_type,
800 detail,
801 event_bus_name,
802 target_account_id,
803 );
804 }
805 }
806
807 pub async fn invoke_lambda(
809 &self,
810 function_arn: &str,
811 payload: &str,
812 ) -> Option<Result<Vec<u8>, String>> {
813 if let Some(ref invoker) = self.lambda_invoker {
814 Some(invoker.invoke_lambda(function_arn, payload).await)
815 } else {
816 None
817 }
818 }
819
820 pub fn send_to_kinesis(&self, stream_arn: &str, data: &str, partition_key: &str) {
822 if let Some(ref sender) = self.kinesis_sender {
823 sender.put_record(stream_arn, data, partition_key);
824 }
825 }
826
827 pub fn start_stepfunctions_execution(&self, state_machine_arn: &str, input: &str) {
829 if let Some(ref starter) = self.stepfunctions_starter {
830 starter.start_execution(state_machine_arn, input);
831 }
832 }
833}
834
835impl Default for DeliveryBus {
836 fn default() -> Self {
837 Self::new()
838 }
839}
840
841#[cfg(test)]
842mod tests {
843 use super::*;
844 use std::sync::atomic::{AtomicUsize, Ordering};
845 use std::sync::Arc;
846
847 struct MockSqs {
849 call_count: AtomicUsize,
850 }
851 impl SqsDelivery for MockSqs {
852 fn deliver_to_queue(
853 &self,
854 _queue_arn: &str,
855 _message_body: &str,
856 _attributes: &HashMap<String, String>,
857 ) {
858 self.call_count.fetch_add(1, Ordering::SeqCst);
859 }
860 }
861
862 struct MockSns {
863 call_count: AtomicUsize,
864 }
865 impl SnsDelivery for MockSns {
866 fn publish_to_topic(&self, _topic_arn: &str, _message: &str, _subject: Option<&str>) {
867 self.call_count.fetch_add(1, Ordering::SeqCst);
868 }
869 }
870
871 struct MockEventBridge {
872 call_count: AtomicUsize,
873 }
874 impl EventBridgeDelivery for MockEventBridge {
875 fn put_event(
876 &self,
877 _source: &str,
878 _detail_type: &str,
879 _detail: &str,
880 _event_bus_name: &str,
881 ) {
882 self.call_count.fetch_add(1, Ordering::SeqCst);
883 }
884 }
885
886 struct MockKinesis {
887 call_count: AtomicUsize,
888 }
889 impl KinesisDelivery for MockKinesis {
890 fn put_record(&self, _stream_arn: &str, _data: &str, _partition_key: &str) {
891 self.call_count.fetch_add(1, Ordering::SeqCst);
892 }
893 }
894
895 struct MockStepFunctions {
896 call_count: AtomicUsize,
897 }
898 impl StepFunctionsDelivery for MockStepFunctions {
899 fn start_execution(&self, _state_machine_arn: &str, _input: &str) {
900 self.call_count.fetch_add(1, Ordering::SeqCst);
901 }
902 }
903
904 #[test]
905 fn delivery_bus_new_has_no_senders() {
906 let bus = DeliveryBus::new();
907 bus.send_to_sqs("arn:queue", "body", &HashMap::new());
909 bus.publish_to_sns("arn:topic", "msg", None);
910 bus.put_event_to_eventbridge("src", "type", "{}", "default");
911 bus.send_to_kinesis("arn:stream", "data", "pk");
912 bus.start_stepfunctions_execution("arn:sfn", "{}");
913 }
915
916 #[test]
917 fn delivery_bus_default_is_same_as_new() {
918 let bus = DeliveryBus::default();
919 bus.send_to_sqs("arn:q", "b", &HashMap::new());
920 }
921
922 #[test]
923 fn send_to_sqs_calls_sender() {
924 let mock = Arc::new(MockSqs {
925 call_count: AtomicUsize::new(0),
926 });
927 let bus = DeliveryBus::new().with_sqs(mock.clone());
928
929 bus.send_to_sqs("arn:queue", "msg", &HashMap::new());
930 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
931
932 bus.send_to_sqs("arn:queue2", "msg2", &HashMap::new());
933 assert_eq!(mock.call_count.load(Ordering::SeqCst), 2);
934 }
935
936 #[test]
937 fn send_to_sqs_with_attrs_calls_sender() {
938 let mock = Arc::new(MockSqs {
939 call_count: AtomicUsize::new(0),
940 });
941 let bus = DeliveryBus::new().with_sqs(mock.clone());
942
943 let mut attrs = HashMap::new();
944 attrs.insert(
945 "key".to_string(),
946 SqsMessageAttribute {
947 data_type: "String".to_string(),
948 string_value: Some("val".to_string()),
949 binary_value: None,
950 },
951 );
952 bus.send_to_sqs_with_attrs("arn:q", "body", &attrs, Some("group"), Some("dedup"));
953 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
954 }
955
956 #[test]
957 fn publish_to_sns_calls_sender() {
958 let mock = Arc::new(MockSns {
959 call_count: AtomicUsize::new(0),
960 });
961 let bus = DeliveryBus::new().with_sns(mock.clone());
962
963 bus.publish_to_sns("arn:topic", "message", Some("subject"));
964 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
965 }
966
967 #[test]
968 fn put_event_to_eventbridge_calls_sender() {
969 let mock = Arc::new(MockEventBridge {
970 call_count: AtomicUsize::new(0),
971 });
972 let bus = DeliveryBus::new().with_eventbridge(mock.clone());
973
974 bus.put_event_to_eventbridge("aws.s3", "Object Created", "{}", "default");
975 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
976 }
977
978 #[test]
979 fn send_to_kinesis_calls_sender() {
980 let mock = Arc::new(MockKinesis {
981 call_count: AtomicUsize::new(0),
982 });
983 let bus = DeliveryBus::new().with_kinesis(mock.clone());
984
985 bus.send_to_kinesis("arn:stream", "data", "partition-key");
986 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
987 }
988
989 #[test]
990 fn start_stepfunctions_calls_sender() {
991 let mock = Arc::new(MockStepFunctions {
992 call_count: AtomicUsize::new(0),
993 });
994 let bus = DeliveryBus::new().with_stepfunctions(mock.clone());
995
996 bus.start_stepfunctions_execution("arn:sfn:machine", r#"{"key":"val"}"#);
997 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
998 }
999
1000 #[test]
1001 fn builder_chaining_works() {
1002 let sqs = Arc::new(MockSqs {
1003 call_count: AtomicUsize::new(0),
1004 });
1005 let sns = Arc::new(MockSns {
1006 call_count: AtomicUsize::new(0),
1007 });
1008 let eb = Arc::new(MockEventBridge {
1009 call_count: AtomicUsize::new(0),
1010 });
1011 let kin = Arc::new(MockKinesis {
1012 call_count: AtomicUsize::new(0),
1013 });
1014 let sfn = Arc::new(MockStepFunctions {
1015 call_count: AtomicUsize::new(0),
1016 });
1017
1018 let bus = DeliveryBus::new()
1019 .with_sqs(sqs.clone())
1020 .with_sns(sns.clone())
1021 .with_eventbridge(eb.clone())
1022 .with_kinesis(kin.clone())
1023 .with_stepfunctions(sfn.clone());
1024
1025 bus.send_to_sqs("q", "m", &HashMap::new());
1026 bus.publish_to_sns("t", "m", None);
1027 bus.put_event_to_eventbridge("s", "d", "{}", "b");
1028 bus.send_to_kinesis("s", "d", "k");
1029 bus.start_stepfunctions_execution("sm", "{}");
1030
1031 assert_eq!(sqs.call_count.load(Ordering::SeqCst), 1);
1032 assert_eq!(sns.call_count.load(Ordering::SeqCst), 1);
1033 assert_eq!(eb.call_count.load(Ordering::SeqCst), 1);
1034 assert_eq!(kin.call_count.load(Ordering::SeqCst), 1);
1035 assert_eq!(sfn.call_count.load(Ordering::SeqCst), 1);
1036 }
1037
1038 #[tokio::test]
1039 async fn invoke_lambda_returns_none_without_invoker() {
1040 let bus = DeliveryBus::new();
1041 let result = bus.invoke_lambda("arn:lambda", "{}").await;
1042 assert!(result.is_none());
1043 }
1044}