Skip to main content

fakecloud_core/
delivery.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4/// Cross-service message delivery.
5///
6/// Services use this to deliver messages to other services without
7/// direct dependencies between service crates. The server wires up
8/// the delivery functions at startup.
9pub struct DeliveryBus {
10    /// Deliver a message to an SQS queue by ARN.
11    sqs_sender: Option<Arc<dyn SqsDelivery>>,
12    /// Publish a message to an SNS topic by ARN.
13    sns_sender: Option<Arc<dyn SnsDelivery>>,
14    /// Put an event onto an EventBridge bus.
15    eventbridge_sender: Option<Arc<dyn EventBridgeDelivery>>,
16    /// Invoke a Lambda function by ARN.
17    lambda_invoker: Option<Arc<dyn LambdaDelivery>>,
18    /// Put records to a Kinesis Data Stream by ARN.
19    kinesis_sender: Option<Arc<dyn KinesisDelivery>>,
20    /// Start Step Functions executions.
21    stepfunctions_starter: Option<Arc<dyn StepFunctionsDelivery>>,
22    /// Start SageMaker pipeline executions (Scheduler sagemaker target).
23    sagemaker_pipeline_starter: Option<Arc<dyn SageMakerPipelineDelivery>>,
24    /// Write objects to S3 buckets.
25    s3_writer: Option<Arc<dyn S3Delivery>>,
26    /// Put records into Firehose delivery streams.
27    firehose_sender: Option<Arc<dyn FirehoseDelivery>>,
28    /// Send a high-level SES SendEmail call (cross-service universal target).
29    ses_dispatcher: Option<Arc<dyn SesSendEmailDispatcher>>,
30    /// Run an ECS task on a cluster (cross-service universal target).
31    ecs_task_runner: Option<Arc<dyn EcsTaskRunner>>,
32    /// Register/deregister ELBv2 targets from ECS runtime.
33    elbv2_target_registration: Option<Arc<dyn Elbv2TargetRegistration>>,
34    ec2_network_lookup: Option<Arc<dyn Ec2NetworkLookup>>,
35    /// Publish CloudWatch metric data points (CloudWatch Logs metric
36    /// filters extract these on PutLogEvents).
37    cloudwatch_metrics: Option<Arc<dyn CloudwatchDelivery>>,
38    /// Put log events into CloudWatch Logs log groups. Used by Step
39    /// Functions Express execution logging and ECS awslogs driver.
40    cloudwatch_logs: Option<Arc<dyn CloudwatchLogsDelivery>>,
41    /// Verify a Cognito-issued JWT against the user pool that issued it.
42    /// Used by API Gateway v1's `COGNITO_USER_POOLS` authorizer to
43    /// validate signature/expiry/audience and extract claims.
44    cognito_jwt_verifier: Option<Arc<dyn CognitoJwtVerifier>>,
45    /// Encrypt/decrypt via KMS for cross-service SSE (S3 SSE-KMS, SES
46    /// S3Action KmsKeyArn, etc.).
47    kms_hook: Option<Arc<dyn KmsHook>>,
48}
49
50/// Message attribute for SQS delivery from SNS.
51#[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/// Error returned by fallible SQS delivery. Used by Scheduler's DLQ
59/// routing, which must distinguish "target queue missing" from
60/// "delivered successfully" to decide whether to send to the
61/// `DeadLetterConfig.Arn`.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum SqsDeliveryError {
64    /// The target queue ARN did not resolve to any existing queue.
65    QueueNotFound(String),
66    /// The ARN could not be parsed into a valid SQS queue identifier.
67    InvalidArn(String),
68    /// The message violated a constraint required by the target queue
69    /// (e.g. FIFO send missing MessageDeduplicationId without
70    /// content-based dedup enabled). Surfaces as a non-retriable
71    /// failure so the upstream service can route to its configured DLQ.
72    InvalidParameter(String),
73    /// The target refused the delivery for lack of authorization (e.g. a
74    /// Scheduler target bus in another account whose resource policy does
75    /// not allow the schedule's role).
76    AccessDenied(String),
77    /// The target could not take the delivery right now (e.g. a service's
78    /// delivery not wired yet at startup).
79    TargetUnavailable(String),
80}
81
82impl std::fmt::Display for SqsDeliveryError {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        match self {
85            Self::QueueNotFound(arn) => write!(f, "queue not found: {arn}"),
86            Self::InvalidArn(arn) => write!(f, "invalid queue ARN: {arn}"),
87            Self::InvalidParameter(msg) => write!(f, "invalid parameter: {msg}"),
88            Self::AccessDenied(msg) => write!(f, "access denied: {msg}"),
89            Self::TargetUnavailable(msg) => write!(f, "target unavailable: {msg}"),
90        }
91    }
92}
93
94impl std::error::Error for SqsDeliveryError {}
95
96/// Trait for delivering messages to SQS queues.
97pub trait SqsDelivery: Send + Sync {
98    fn deliver_to_queue(
99        &self,
100        queue_arn: &str,
101        message_body: &str,
102        attributes: &HashMap<String, String>,
103    );
104
105    /// Deliver with message attributes and FIFO fields
106    fn deliver_to_queue_with_attrs(
107        &self,
108        queue_arn: &str,
109        message_body: &str,
110        message_attributes: &HashMap<String, SqsMessageAttribute>,
111        message_group_id: Option<&str>,
112        message_dedup_id: Option<&str>,
113    ) {
114        // Default implementation: fall back to simple delivery
115        let _ = (message_attributes, message_group_id, message_dedup_id);
116        self.deliver_to_queue(queue_arn, message_body, &HashMap::new());
117    }
118
119    /// The stored ARN of the queue a QueueUrl (`<endpoint>/<account>/<name>`)
120    /// names, looked up by account and queue name. A fakecloud QueueUrl
121    /// carries no region, so the caller passes the region it is acting in:
122    /// that region's queue wins, else the queue of that account and name in
123    /// the one other region that has it. `None` when there is no such queue
124    /// (or it is ambiguous).
125    fn queue_arn_for_url(&self, region: &str, queue_url: &str) -> Option<String> {
126        let _ = (region, queue_url);
127        None
128    }
129
130    /// Fallible variant used by Scheduler's DLQ routing. Default
131    /// implementation assumes the queue exists (preserving the
132    /// fire-and-forget semantics of `deliver_to_queue`); the real SQS
133    /// impl overrides this to actually look up the queue and report
134    /// `QueueNotFound` so the caller can route to a DLQ.
135    fn try_deliver_to_queue_with_attrs(
136        &self,
137        queue_arn: &str,
138        message_body: &str,
139        message_attributes: &HashMap<String, SqsMessageAttribute>,
140        message_group_id: Option<&str>,
141        message_dedup_id: Option<&str>,
142    ) -> Result<(), SqsDeliveryError> {
143        self.deliver_to_queue_with_attrs(
144            queue_arn,
145            message_body,
146            message_attributes,
147            message_group_id,
148            message_dedup_id,
149        );
150        Ok(())
151    }
152}
153
154/// Trait for publishing messages to SNS topics.
155pub trait SnsDelivery: Send + Sync {
156    fn publish_to_topic(&self, topic_arn: &str, message: &str, subject: Option<&str>);
157
158    /// Publish to a FIFO SNS topic carrying the message group/dedup IDs
159    /// that downstream SQS subscribers need for ordering. Default impl
160    /// drops the IDs so non-FIFO callers don't have to override.
161    fn publish_to_topic_fifo(
162        &self,
163        topic_arn: &str,
164        message: &str,
165        subject: Option<&str>,
166        _message_group_id: Option<&str>,
167        _message_dedup_id: Option<&str>,
168    ) {
169        self.publish_to_topic(topic_arn, message, subject);
170    }
171}
172
173/// An event another service puts onto an EventBridge bus (S3 bucket
174/// notifications, ECS task state changes, Scheduler/Pipes/Step Functions
175/// targets, ...).
176///
177/// `account_id` and `region` name where the event *originates*: the owner
178/// account and region of the emitting resource. They are stamped into the
179/// event's `account` and `region` fields and are what rule patterns match on,
180/// exactly as AWS does for service events. There is deliberately no
181/// "default account" form: every caller knows the resource the event is
182/// about, and an event that silently lands on another account's bus is
183/// invisible to the rules that should have fired.
184#[derive(Debug, Clone, Copy)]
185pub struct CrossServiceEvent<'a> {
186    pub source: &'a str,
187    pub detail_type: &'a str,
188    /// JSON detail document.
189    pub detail: &'a str,
190    /// Bus name (a bus owned by `account_id`) or a full event-bus ARN. An ARN
191    /// routes the event to the bus owner's account (a cross-account target),
192    /// while the event keeps its originating `account` / `region`.
193    pub event_bus: &'a str,
194    /// Account the event originates in.
195    pub account_id: &'a str,
196    /// Region the event originates in.
197    pub region: &'a str,
198    /// ARNs of the resources the event is about (`resources` field).
199    pub resources: &'a [String],
200    /// The IAM role the source acts as when it puts the event (a schedule's
201    /// or pipe's role, a state machine's execution role). A bus in another
202    /// account authorizes the put against this principal through its
203    /// resource policy; `None` means the originating account's root.
204    pub principal_arn: Option<&'a str>,
205}
206
207/// Why EventBridge refused a cross-service event.
208#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
209pub enum EventBridgeDeliveryError {
210    /// The target bus belongs to another account and its resource policy
211    /// does not allow the source's principal to put events on it. The event
212    /// is not stored, archived or delivered.
213    #[error("AccessDeniedException: {0}")]
214    AccessDenied(String),
215    /// EventBridge could not take the event (e.g. delivery not wired yet at
216    /// startup). The event is dropped; the source records a failed delivery.
217    #[error("InternalException: {0}")]
218    Unavailable(String),
219}
220
221/// Trait for putting events onto an EventBridge bus from cross-service integrations.
222pub trait EventBridgeDelivery: Send + Sync {
223    /// Put `event` onto the bus it names, scoped to the account that owns that
224    /// bus. The implementation handles rule matching and target delivery.
225    /// Returns the new event's ID, or why the put was refused.
226    fn put_event(&self, event: &CrossServiceEvent<'_>) -> Result<String, EventBridgeDeliveryError>;
227}
228
229/// Trait for invoking Lambda functions from cross-service integrations.
230pub trait LambdaDelivery: Send + Sync {
231    /// Invoke a Lambda function with the given payload.
232    /// The function is identified by ARN. Returns the response bytes on success.
233    fn invoke_lambda(
234        &self,
235        function_arn: &str,
236        payload: &str,
237    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<u8>, String>> + Send>>;
238}
239
240/// Trait for putting records to Kinesis Data Streams.
241pub trait KinesisDelivery: Send + Sync {
242    /// Put a record to a Kinesis stream identified by ARN.
243    /// The data should be base64-encoded. partition_key is used for shard distribution.
244    fn put_record(&self, stream_arn: &str, data: &str, partition_key: &str);
245}
246
247/// Trait for starting Step Functions executions from cross-service integrations.
248pub trait StepFunctionsDelivery: Send + Sync {
249    /// Start a state machine execution with the given input.
250    /// The state machine is identified by ARN.
251    fn start_execution(&self, state_machine_arn: &str, input: &str);
252}
253
254/// Cross-service SageMaker pipeline dispatch used by EventBridge Scheduler's
255/// `sagemaker:pipeline` target. The pipeline is identified by ARN
256/// (`arn:aws:sagemaker:<region>:<account>:pipeline/<name>`); `parameters` is the
257/// target's `PipelineParameterList`.
258pub trait SageMakerPipelineDelivery: Send + Sync {
259    fn start_pipeline_execution(&self, pipeline_arn: &str, parameters: &serde_json::Value);
260}
261
262/// Cross-service Kinesis Data Firehose dispatch used by services
263/// (CloudWatch Logs subscription filters, EventBridge targets) that
264/// route records into a delivery stream without depending on the
265/// firehose crate directly. ARN form is
266/// `arn:aws:firehose:<region>:<account>:deliverystream/<name>`.
267pub trait FirehoseDelivery: Send + Sync {
268    fn put_record(&self, delivery_stream_arn: &str, data: &[u8]);
269}
270
271/// Cross-service S3 writer used by services that need to deliver
272/// content to S3 buckets without taking a direct dep on the S3 crate
273/// (CloudWatch Logs export tasks, Kinesis Firehose, ELB access logs).
274pub trait S3Delivery: Send + Sync {
275    /// Put an object to a bucket. Returns the bucket name on success
276    /// or an error string the caller can surface in tests / logs.
277    fn put_object(
278        &self,
279        account_id: &str,
280        bucket: &str,
281        key: &str,
282        body: Vec<u8>,
283        content_type: Option<&str>,
284    ) -> Result<(), String>;
285
286    /// Read an object's body. Returns Err when the bucket or key does
287    /// not exist or when the body cannot be read. Used by RDS
288    /// `RestoreDBInstanceFromS3` to ingest a backup blob without taking
289    /// a direct dep on the S3 crate.
290    fn get_object(&self, account_id: &str, bucket: &str, key: &str) -> Result<Vec<u8>, String>;
291}
292
293/// SES SendEmail dispatch for callers that already speak the AWS SES
294/// SendEmail / SendEmailV2 shape (multiple to/cc/bcc, optional subject,
295/// text/html bodies). Distinct from `EmailDispatcher`, which is the
296/// single-recipient cross-service primitive used by Cognito.
297pub trait SesSendEmailDispatcher: Send + Sync {
298    #[allow(clippy::too_many_arguments)]
299    fn send_email(
300        &self,
301        account_id: &str,
302        from: &str,
303        to: Vec<String>,
304        cc: Vec<String>,
305        bcc: Vec<String>,
306        subject: Option<&str>,
307        text_body: Option<&str>,
308        html_body: Option<&str>,
309    ) -> Result<(), String>;
310}
311
312/// Synthesize an ECS RunTask call from outside the ECS crate. Used by
313/// EventBridge Scheduler and EventBridge Rules to start tasks without
314/// depending directly on `fakecloud_ecs`.
315pub trait EcsTaskRunner: Send + Sync {
316    fn run_task(
317        &self,
318        account_id: &str,
319        cluster: &str,
320        task_definition: &str,
321        launch_type: Option<&str>,
322        count: usize,
323    ) -> Result<(), String>;
324}
325
326/// Register/deregister targets with ELBv2 target groups from outside
327/// the elbv2 crate. Used by ECS runtime when tasks with load balancers
328/// reach RUNNING or STOPPED.
329pub trait Elbv2TargetRegistration: Send + Sync {
330    fn register_targets(
331        &self,
332        account_id: &str,
333        target_group_arn: &str,
334        targets: Vec<(String, Option<i64>)>,
335    );
336    fn deregister_targets(
337        &self,
338        account_id: &str,
339        target_group_arn: &str,
340        targets: Vec<(String, Option<i64>)>,
341    );
342}
343
344/// A requester-managed network interface EC2 created for a task.
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct TaskEni {
347    pub eni_id: String,
348    pub private_ip: String,
349    pub mac_address: String,
350}
351
352/// EC2 VPC networking from outside the ec2 crate. Used by the ECS runtime to
353/// give an `awsvpc` task a real ENI in its subnet (visible to
354/// DescribeNetworkInterfaces, its private IP allocated by EC2 like any other
355/// interface's) and to delete it when the task stops.
356pub trait Ec2NetworkLookup: Send + Sync {
357    /// Create a requester-managed ENI in `subnet_id`. `Err` when the subnet
358    /// doesn't exist or has no free address.
359    fn create_task_eni(
360        &self,
361        account_id: &str,
362        subnet_id: &str,
363        group_ids: Vec<String>,
364        description: String,
365    ) -> Result<TaskEni, String>;
366    /// Delete an ENI [`Self::create_task_eni`] created (no-op otherwise).
367    fn delete_task_eni(&self, account_id: &str, eni_id: &str);
368}
369
370/// Publish CloudWatch metric data points from outside the cloudwatch
371/// crate. Used by CloudWatch Logs metric filters when an incoming log
372/// event matches their pattern.
373pub trait CloudwatchDelivery: Send + Sync {
374    #[allow(clippy::too_many_arguments)]
375    fn put_metric(
376        &self,
377        account_id: &str,
378        region: &str,
379        namespace: &str,
380        metric_name: &str,
381        value: f64,
382        unit: Option<&str>,
383        dimensions: std::collections::BTreeMap<String, String>,
384        timestamp_ms: i64,
385    );
386}
387
388/// Put log events into CloudWatch Logs log groups from outside the
389/// logs crate. Used by Step Functions Express execution logging and
390/// ECS awslogs driver so they can deliver without depending directly
391/// on fakecloud_logs.
392pub trait CloudwatchLogsDelivery: Send + Sync {
393    fn put_log_events(
394        &self,
395        account_id: &str,
396        region: &str,
397        log_group_name: &str,
398        log_stream_name: &str,
399        events: &[(i64, String)],
400    );
401}
402
403/// Outbound email dispatch used by services that emulate AWS flows that
404/// route through SES (Cognito verification, etc.) without taking a direct
405/// dependency on the SES crate.
406pub trait EmailDispatcher: Send + Sync {
407    fn send_email(
408        &self,
409        account_id: &str,
410        from: &str,
411        to: &str,
412        subject: &str,
413        body_text: &str,
414        body_html: Option<&str>,
415    );
416}
417
418/// Outbound SMS dispatch used by services that emulate AWS flows that route
419/// through SNS phone-number publish (Cognito SMS MFA, etc.).
420pub trait SmsDispatcher: Send + Sync {
421    /// `region` is the region the SMS is sent from (the SNS account settings
422    /// and recorded messages are per region).
423    fn send_sms(&self, account_id: &str, region: &str, phone_number: &str, message: &str);
424}
425
426/// Cross-service KMS hook: services that accept a `KmsKeyId` (Secrets
427/// Manager, SSM `SecureString`, S3 SSE-KMS, SQS / SNS / DynamoDB
428/// encrypted resources) call this so that real KMS calls happen, the
429/// invocation is recorded for introspection, and the returned blob is
430/// decryptable by the public KMS API.
431///
432/// Encryption context is the AWS-defined per-service map (e.g.
433/// `{aws:secretsmanager:secretArn: <arn>}` for Secrets Manager) and is
434/// recorded with the call so test code can assert it.
435pub trait KmsHook: Send + Sync {
436    fn encrypt(
437        &self,
438        account_id: &str,
439        region: &str,
440        key_id: &str,
441        plaintext: &[u8],
442        service_principal: &str,
443        encryption_context: std::collections::HashMap<String, String>,
444    ) -> Result<String, String>;
445
446    fn decrypt(
447        &self,
448        account_id: &str,
449        ciphertext_b64: &str,
450        service_principal: &str,
451        encryption_context: std::collections::HashMap<String, String>,
452    ) -> Result<Vec<u8>, String>;
453
454    /// Resolve `key_id` (key id, key ARN, alias name or alias ARN) to the
455    /// key's ARN, provisioning the AWS-managed `aws/<service>` key on first
456    /// use. Services report the resolved ARN on their resource descriptions.
457    fn resolve_key_arn(
458        &self,
459        _account_id: &str,
460        _region: &str,
461        _key_id: &str,
462        _service_principal: &str,
463    ) -> Result<String, String> {
464        Err("key resolution is not supported by this hook".to_string())
465    }
466
467    /// The ARN of the AWS-managed key for `service` (the key behind
468    /// `alias/aws/<service>`) in `account_id` and `region`, minted on first
469    /// use. AWS-managed keys exist once per account AND region, so each
470    /// region gets its own key in its own partition.
471    fn aws_managed_key_arn(
472        &self,
473        _account_id: &str,
474        _region: &str,
475        _service: &str,
476        _service_principal: &str,
477    ) -> Result<String, String> {
478        Err("AWS-managed key resolution is not supported by this hook".to_string())
479    }
480}
481
482/// The ARN of the AWS-managed KMS key for `service` (the key behind
483/// `alias/aws/<service>`) in `account_id` and `region`, which a service reports
484/// for a resource encrypted at rest when the caller named no key. The key is
485/// resolved through the KMS hook, which mints it on first use in that account
486/// and region (and the region's partition), as real KMS does, so the ARN is a
487/// real key `DescribeKey` / `Decrypt` accept. `None` when no hook is wired (a
488/// service running outside the server) or resolution fails; a failure is
489/// logged, never swallowed.
490pub fn aws_managed_kms_key_arn(
491    hook: Option<&dyn KmsHook>,
492    account_id: &str,
493    region: &str,
494    service: &str,
495) -> Option<String> {
496    let hook = hook?;
497    let alias = format!("alias/aws/{service}");
498    let principal = format!("{service}.amazonaws.com");
499    match hook.aws_managed_key_arn(account_id, region, service, &principal) {
500        Ok(arn) => Some(arn),
501        Err(err) => {
502            tracing::warn!(
503                %err,
504                account_id,
505                region,
506                alias = %alias,
507                "could not resolve the AWS-managed KMS key; reporting no key"
508            );
509            None
510        }
511    }
512}
513
514/// The KMS key a resource is encrypted with: the caller-named key (`named`,
515/// ignored when empty) or, when none is named, the AWS-managed key for
516/// `service` in `account_id` and `region` (see [`aws_managed_kms_key_arn`]).
517pub fn kms_key_or_aws_managed(
518    hook: Option<&dyn KmsHook>,
519    named: Option<&str>,
520    account_id: &str,
521    region: &str,
522    service: &str,
523) -> Option<String> {
524    match named.filter(|k| !k.is_empty()) {
525        Some(key) => Some(key.to_string()),
526        None => aws_managed_kms_key_arn(hook, account_id, region, service),
527    }
528}
529
530/// [`kms_key_or_aws_managed`] for services that report the key as a key ARN
531/// whatever form the caller named it in (RDS, DocumentDB, Neptune, EBS): a
532/// named key is resolved with [`resolve_named_kms_key_arn`].
533pub fn kms_key_arn_or_aws_managed(
534    hook: Option<&dyn KmsHook>,
535    named: Option<&str>,
536    account_id: &str,
537    region: &str,
538    service: &str,
539) -> Option<String> {
540    match named.filter(|k| !k.is_empty()) {
541        Some(key) => Some(resolve_named_kms_key_arn(
542            hook, key, account_id, region, service,
543        )),
544        None => aws_managed_kms_key_arn(hook, account_id, region, service),
545    }
546}
547
548/// The ARN of the key a caller named (key id, key ARN, alias name or alias
549/// ARN) as KMS resolves it: an alias reports its target key's ARN, and an
550/// AWS-managed alias (`alias/aws/<svc>`) that region's AWS-managed key, minted
551/// on first use. A key KMS cannot resolve (one never created in fakecloud, one
552/// in another region than `region`, or no hook wired) is reported as given.
553pub fn resolve_named_kms_key_arn(
554    hook: Option<&dyn KmsHook>,
555    key: &str,
556    account_id: &str,
557    region: &str,
558    service: &str,
559) -> String {
560    let Some(h) = hook else {
561        return key.to_string();
562    };
563    // A KMS ARN naming another region is a key the resource's region cannot
564    // use: report it as given rather than resolving (or minting) anything in
565    // that other region.
566    if let Some(rest) = key.strip_prefix("arn:") {
567        if rest.split(':').nth(2).is_some_and(|r| r != region) {
568            return key.to_string();
569        }
570    }
571    // KMS resolves an alias name in `region` and an alias ARN in the region
572    // it names; `alias/aws/<svc>` resolves to (minting on first use) that
573    // region's AWS-managed key for `<svc>`, provisioned for that service.
574    let resource = key.rsplit(':').next().unwrap_or(key);
575    let owner = resource
576        .strip_prefix("alias/aws/")
577        .filter(|s| !s.is_empty())
578        .unwrap_or(service);
579    let principal = format!("{owner}.amazonaws.com");
580    match h.resolve_key_arn(account_id, region, key, &principal) {
581        Ok(arn) => arn,
582        Err(err) => {
583            tracing::debug!(%err, key, "KMS key not resolvable; reporting it as given");
584            key.to_string()
585        }
586    }
587}
588
589/// Cognito-issued JWT verification hook. Implementations are wired by
590/// fakecloud-server and back the `COGNITO_USER_POOLS` authorizer in
591/// API Gateway v1. The verifier validates RS256 signature, exp/nbf,
592/// `iss`, and `aud`/`client_id` against the user pool referenced by
593/// the authorizer's `providerArns`. On success returns the decoded
594/// claims as a JSON object; on failure returns an error string the
595/// caller surfaces as `401 Unauthorized`.
596pub trait CognitoJwtVerifier: Send + Sync {
597    fn verify_token(
598        &self,
599        account_id: &str,
600        user_pool_arn: &str,
601        token: &str,
602    ) -> Result<serde_json::Value, String>;
603}
604
605impl DeliveryBus {
606    pub fn new() -> Self {
607        Self {
608            sqs_sender: None,
609            sns_sender: None,
610            eventbridge_sender: None,
611            lambda_invoker: None,
612            kinesis_sender: None,
613            stepfunctions_starter: None,
614            sagemaker_pipeline_starter: None,
615            s3_writer: None,
616            firehose_sender: None,
617            ses_dispatcher: None,
618            ecs_task_runner: None,
619            elbv2_target_registration: None,
620            ec2_network_lookup: None,
621            cloudwatch_metrics: None,
622            cloudwatch_logs: None,
623            cognito_jwt_verifier: None,
624            kms_hook: None,
625        }
626    }
627
628    pub fn with_cognito_jwt_verifier(mut self, verifier: Arc<dyn CognitoJwtVerifier>) -> Self {
629        self.cognito_jwt_verifier = Some(verifier);
630        self
631    }
632
633    pub fn with_kms_hook(mut self, hook: Arc<dyn KmsHook>) -> Self {
634        self.kms_hook = Some(hook);
635        self
636    }
637
638    /// Encrypt plaintext with the supplied KMS key. Returns Err when no
639    /// KMS hook is wired or the encryption fails.
640    pub fn kms_encrypt(
641        &self,
642        account_id: &str,
643        region: &str,
644        key_id: &str,
645        plaintext: &[u8],
646        service_principal: &str,
647        encryption_context: std::collections::HashMap<String, String>,
648    ) -> Result<String, String> {
649        match self.kms_hook {
650            Some(ref h) => h.encrypt(
651                account_id,
652                region,
653                key_id,
654                plaintext,
655                service_principal,
656                encryption_context,
657            ),
658            None => Err("KMS hook not configured".to_string()),
659        }
660    }
661
662    /// Decrypt a KMS ciphertext blob. Returns Err when no KMS hook is
663    /// wired or the decryption fails.
664    pub fn kms_decrypt(
665        &self,
666        account_id: &str,
667        ciphertext_b64: &str,
668        service_principal: &str,
669        encryption_context: std::collections::HashMap<String, String>,
670    ) -> Result<Vec<u8>, String> {
671        match self.kms_hook {
672            Some(ref h) => h.decrypt(
673                account_id,
674                ciphertext_b64,
675                service_principal,
676                encryption_context,
677            ),
678            None => Err("KMS hook not configured".to_string()),
679        }
680    }
681
682    /// Verify a Cognito JWT against a user pool. Returns `Err` when no
683    /// verifier is wired or when the token fails validation.
684    pub fn verify_cognito_jwt(
685        &self,
686        account_id: &str,
687        user_pool_arn: &str,
688        token: &str,
689    ) -> Result<serde_json::Value, String> {
690        match self.cognito_jwt_verifier {
691            Some(ref v) => v.verify_token(account_id, user_pool_arn, token),
692            None => Err("Cognito JWT verifier not configured".to_string()),
693        }
694    }
695
696    pub fn with_cloudwatch_metrics(mut self, sender: Arc<dyn CloudwatchDelivery>) -> Self {
697        self.cloudwatch_metrics = Some(sender);
698        self
699    }
700
701    /// Publish a CloudWatch metric data point. Silently no-ops when no
702    /// CloudWatch sender is wired (in-process tests not exercising the
703    /// metrics path).
704    #[allow(clippy::too_many_arguments)]
705    pub fn put_cloudwatch_metric(
706        &self,
707        account_id: &str,
708        region: &str,
709        namespace: &str,
710        metric_name: &str,
711        value: f64,
712        unit: Option<&str>,
713        dimensions: std::collections::BTreeMap<String, String>,
714        timestamp_ms: i64,
715    ) {
716        if let Some(ref sender) = self.cloudwatch_metrics {
717            sender.put_metric(
718                account_id,
719                region,
720                namespace,
721                metric_name,
722                value,
723                unit,
724                dimensions,
725                timestamp_ms,
726            );
727        }
728    }
729
730    pub fn with_cloudwatch_logs(mut self, sender: Arc<dyn CloudwatchLogsDelivery>) -> Self {
731        self.cloudwatch_logs = Some(sender);
732        self
733    }
734
735    /// Put log events to a CloudWatch Logs log group / stream.
736    /// Silently no-ops when no CloudWatch Logs sender is wired.
737    pub fn put_log_events(
738        &self,
739        account_id: &str,
740        region: &str,
741        log_group_name: &str,
742        log_stream_name: &str,
743        events: &[(i64, String)],
744    ) {
745        if let Some(ref sender) = self.cloudwatch_logs {
746            sender.put_log_events(account_id, region, log_group_name, log_stream_name, events);
747        }
748    }
749
750    pub fn with_ses_dispatcher(mut self, dispatcher: Arc<dyn SesSendEmailDispatcher>) -> Self {
751        self.ses_dispatcher = Some(dispatcher);
752        self
753    }
754
755    pub fn with_ecs_task_runner(mut self, runner: Arc<dyn EcsTaskRunner>) -> Self {
756        self.ecs_task_runner = Some(runner);
757        self
758    }
759
760    pub fn with_elbv2_target_registration(mut self, reg: Arc<dyn Elbv2TargetRegistration>) -> Self {
761        self.elbv2_target_registration = Some(reg);
762        self
763    }
764
765    pub fn with_ec2_network_lookup(mut self, lookup: Arc<dyn Ec2NetworkLookup>) -> Self {
766        self.ec2_network_lookup = Some(lookup);
767        self
768    }
769
770    /// Create a task ENI in an EC2 subnet. `None` when no EC2 lookup is
771    /// wired; `Some(Err)` when EC2 refused (unknown subnet, subnet full).
772    pub fn create_task_eni(
773        &self,
774        account_id: &str,
775        subnet_id: &str,
776        group_ids: Vec<String>,
777        description: String,
778    ) -> Option<Result<TaskEni, String>> {
779        self.ec2_network_lookup
780            .as_ref()
781            .map(|l| l.create_task_eni(account_id, subnet_id, group_ids, description))
782    }
783
784    /// Delete a task ENI created with [`Self::create_task_eni`].
785    pub fn delete_task_eni(&self, account_id: &str, eni_id: &str) {
786        if let Some(l) = &self.ec2_network_lookup {
787            l.delete_task_eni(account_id, eni_id);
788        }
789    }
790
791    /// Register targets with an ELBv2 target group. Silently no-ops when
792    /// no ELBv2 target registration hook is wired.
793    pub fn register_elbv2_targets(
794        &self,
795        account_id: &str,
796        target_group_arn: &str,
797        targets: Vec<(String, Option<i64>)>,
798    ) {
799        if let Some(ref reg) = self.elbv2_target_registration {
800            reg.register_targets(account_id, target_group_arn, targets);
801        }
802    }
803
804    /// Deregister targets from an ELBv2 target group. Silently no-ops
805    /// when no ELBv2 target registration hook is wired.
806    pub fn deregister_elbv2_targets(
807        &self,
808        account_id: &str,
809        target_group_arn: &str,
810        targets: Vec<(String, Option<i64>)>,
811    ) {
812        if let Some(ref reg) = self.elbv2_target_registration {
813            reg.deregister_targets(account_id, target_group_arn, targets);
814        }
815    }
816
817    /// Send an email via SES. Returns `Err` when no SES dispatcher is
818    /// wired or the underlying impl rejects (bad source/dest).
819    #[allow(clippy::too_many_arguments)]
820    pub fn send_ses_email(
821        &self,
822        account_id: &str,
823        from: &str,
824        to: Vec<String>,
825        cc: Vec<String>,
826        bcc: Vec<String>,
827        subject: Option<&str>,
828        text_body: Option<&str>,
829        html_body: Option<&str>,
830    ) -> Result<(), String> {
831        match self.ses_dispatcher {
832            Some(ref d) => {
833                d.send_email(account_id, from, to, cc, bcc, subject, text_body, html_body)
834            }
835            None => Err("SES dispatcher not configured".to_string()),
836        }
837    }
838
839    /// Run an ECS task. Returns `Err` when no ECS runner is wired or
840    /// the impl rejects (unknown cluster / task definition).
841    pub fn run_ecs_task(
842        &self,
843        account_id: &str,
844        cluster: &str,
845        task_definition: &str,
846        launch_type: Option<&str>,
847        count: usize,
848    ) -> Result<(), String> {
849        match self.ecs_task_runner {
850            Some(ref r) => r.run_task(account_id, cluster, task_definition, launch_type, count),
851            None => Err("ECS task runner not configured".to_string()),
852        }
853    }
854
855    pub fn with_s3(mut self, sender: Arc<dyn S3Delivery>) -> Self {
856        self.s3_writer = Some(sender);
857        self
858    }
859
860    pub fn with_firehose(mut self, sender: Arc<dyn FirehoseDelivery>) -> Self {
861        self.firehose_sender = Some(sender);
862        self
863    }
864
865    /// Send a single record to a Firehose delivery stream by ARN.
866    /// Silently no-ops when no Firehose sender is wired (in-process
867    /// tests). Production wiring goes through fakecloud_firehose.
868    pub fn put_record_to_firehose(&self, delivery_stream_arn: &str, data: &[u8]) {
869        if let Some(ref sender) = self.firehose_sender {
870            sender.put_record(delivery_stream_arn, data);
871        }
872    }
873
874    /// Write content to S3. Returns Err when no S3 writer is wired or
875    /// when the underlying impl rejects the bucket / payload.
876    pub fn put_object_to_s3(
877        &self,
878        account_id: &str,
879        bucket: &str,
880        key: &str,
881        body: Vec<u8>,
882        content_type: Option<&str>,
883    ) -> Result<(), String> {
884        match self.s3_writer {
885            Some(ref sender) => sender.put_object(account_id, bucket, key, body, content_type),
886            None => Err("S3 writer not configured".to_string()),
887        }
888    }
889
890    /// Read content from S3. Returns Err when no S3 client is wired or
891    /// when the underlying impl cannot resolve the object.
892    pub fn get_object_from_s3(
893        &self,
894        account_id: &str,
895        bucket: &str,
896        key: &str,
897    ) -> Result<Vec<u8>, String> {
898        match self.s3_writer {
899            Some(ref sender) => sender.get_object(account_id, bucket, key),
900            None => Err("S3 client not configured".to_string()),
901        }
902    }
903
904    pub fn with_sqs(mut self, sender: Arc<dyn SqsDelivery>) -> Self {
905        self.sqs_sender = Some(sender);
906        self
907    }
908
909    pub fn with_sns(mut self, sender: Arc<dyn SnsDelivery>) -> Self {
910        self.sns_sender = Some(sender);
911        self
912    }
913
914    pub fn with_eventbridge(mut self, sender: Arc<dyn EventBridgeDelivery>) -> Self {
915        self.eventbridge_sender = Some(sender);
916        self
917    }
918
919    pub fn with_lambda(mut self, invoker: Arc<dyn LambdaDelivery>) -> Self {
920        self.lambda_invoker = Some(invoker);
921        self
922    }
923
924    pub fn with_kinesis(mut self, sender: Arc<dyn KinesisDelivery>) -> Self {
925        self.kinesis_sender = Some(sender);
926        self
927    }
928
929    /// Put a record to a Kinesis Data Stream identified by ARN.
930    /// Silently no-ops when no Kinesis sender is wired.
931    pub fn put_record_to_kinesis(&self, stream_arn: &str, data: &str, partition_key: &str) {
932        if let Some(ref sender) = self.kinesis_sender {
933            sender.put_record(stream_arn, data, partition_key);
934        }
935    }
936
937    pub fn with_sagemaker_pipeline(mut self, starter: Arc<dyn SageMakerPipelineDelivery>) -> Self {
938        self.sagemaker_pipeline_starter = Some(starter);
939        self
940    }
941
942    /// Start a SageMaker pipeline execution if a starter is wired.
943    pub fn start_sagemaker_pipeline(&self, pipeline_arn: &str, parameters: &serde_json::Value) {
944        if let Some(ref starter) = self.sagemaker_pipeline_starter {
945            starter.start_pipeline_execution(pipeline_arn, parameters);
946        }
947    }
948
949    pub fn with_stepfunctions(mut self, starter: Arc<dyn StepFunctionsDelivery>) -> Self {
950        self.stepfunctions_starter = Some(starter);
951        self
952    }
953
954    /// The stored ARN of the SQS queue a QueueUrl names in `region`, if it
955    /// exists.
956    pub fn sqs_queue_arn_for_url(&self, region: &str, queue_url: &str) -> Option<String> {
957        self.sqs_sender
958            .as_ref()?
959            .queue_arn_for_url(region, queue_url)
960    }
961
962    /// Send a message to an SQS queue identified by ARN.
963    pub fn send_to_sqs(
964        &self,
965        queue_arn: &str,
966        message_body: &str,
967        attributes: &HashMap<String, String>,
968    ) {
969        if let Some(ref sender) = self.sqs_sender {
970            sender.deliver_to_queue(queue_arn, message_body, attributes);
971        }
972    }
973
974    /// Send a message to an SQS queue with message attributes and FIFO fields.
975    pub fn send_to_sqs_with_attrs(
976        &self,
977        queue_arn: &str,
978        message_body: &str,
979        message_attributes: &HashMap<String, SqsMessageAttribute>,
980        message_group_id: Option<&str>,
981        message_dedup_id: Option<&str>,
982    ) {
983        if let Some(ref sender) = self.sqs_sender {
984            sender.deliver_to_queue_with_attrs(
985                queue_arn,
986                message_body,
987                message_attributes,
988                message_group_id,
989                message_dedup_id,
990            );
991        }
992    }
993
994    /// Fallible SQS send — returns `Err` when the target queue does not
995    /// exist, so callers (Scheduler) can route to a DLQ. Returns
996    /// `Err(QueueNotFound)` when no SQS sender is wired up at all,
997    /// matching the "target unreachable" semantics Scheduler relies on.
998    pub fn try_send_to_sqs_with_attrs(
999        &self,
1000        queue_arn: &str,
1001        message_body: &str,
1002        message_attributes: &HashMap<String, SqsMessageAttribute>,
1003        message_group_id: Option<&str>,
1004        message_dedup_id: Option<&str>,
1005    ) -> Result<(), SqsDeliveryError> {
1006        match self.sqs_sender {
1007            Some(ref sender) => sender.try_deliver_to_queue_with_attrs(
1008                queue_arn,
1009                message_body,
1010                message_attributes,
1011                message_group_id,
1012                message_dedup_id,
1013            ),
1014            None => Err(SqsDeliveryError::QueueNotFound(queue_arn.to_string())),
1015        }
1016    }
1017
1018    /// Publish a message to an SNS topic identified by ARN.
1019    pub fn publish_to_sns(&self, topic_arn: &str, message: &str, subject: Option<&str>) {
1020        if let Some(ref sender) = self.sns_sender {
1021            sender.publish_to_topic(topic_arn, message, subject);
1022        }
1023    }
1024
1025    /// Put a cross-service event onto an EventBridge bus (see
1026    /// [`CrossServiceEvent`] for how the bus and account are resolved).
1027    /// `Ok(None)` when no EventBridge sender is wired.
1028    pub fn put_event_to_eventbridge(
1029        &self,
1030        event: &CrossServiceEvent<'_>,
1031    ) -> Result<Option<String>, EventBridgeDeliveryError> {
1032        match self.eventbridge_sender {
1033            Some(ref sender) => sender.put_event(event).map(Some),
1034            None => Ok(None),
1035        }
1036    }
1037
1038    /// Invoke a Lambda function identified by ARN.
1039    pub async fn invoke_lambda(
1040        &self,
1041        function_arn: &str,
1042        payload: &str,
1043    ) -> Option<Result<Vec<u8>, String>> {
1044        if let Some(ref invoker) = self.lambda_invoker {
1045            Some(invoker.invoke_lambda(function_arn, payload).await)
1046        } else {
1047            None
1048        }
1049    }
1050
1051    /// Put a record to a Kinesis stream identified by ARN.
1052    pub fn send_to_kinesis(&self, stream_arn: &str, data: &str, partition_key: &str) {
1053        if let Some(ref sender) = self.kinesis_sender {
1054            sender.put_record(stream_arn, data, partition_key);
1055        }
1056    }
1057
1058    /// Start a Step Functions execution identified by state machine ARN.
1059    pub fn start_stepfunctions_execution(&self, state_machine_arn: &str, input: &str) {
1060        if let Some(ref starter) = self.stepfunctions_starter {
1061            starter.start_execution(state_machine_arn, input);
1062        }
1063    }
1064}
1065
1066impl Default for DeliveryBus {
1067    fn default() -> Self {
1068        Self::new()
1069    }
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074    use super::*;
1075    use std::sync::atomic::{AtomicUsize, Ordering};
1076    use std::sync::Arc;
1077
1078    // Mock implementations
1079    struct MockSqs {
1080        call_count: AtomicUsize,
1081    }
1082    impl SqsDelivery for MockSqs {
1083        fn deliver_to_queue(
1084            &self,
1085            _queue_arn: &str,
1086            _message_body: &str,
1087            _attributes: &HashMap<String, String>,
1088        ) {
1089            self.call_count.fetch_add(1, Ordering::SeqCst);
1090        }
1091    }
1092
1093    struct MockSns {
1094        call_count: AtomicUsize,
1095    }
1096    impl SnsDelivery for MockSns {
1097        fn publish_to_topic(&self, _topic_arn: &str, _message: &str, _subject: Option<&str>) {
1098            self.call_count.fetch_add(1, Ordering::SeqCst);
1099        }
1100    }
1101
1102    struct MockEventBridge {
1103        call_count: AtomicUsize,
1104    }
1105    impl EventBridgeDelivery for MockEventBridge {
1106        fn put_event(
1107            &self,
1108            _event: &CrossServiceEvent<'_>,
1109        ) -> Result<String, EventBridgeDeliveryError> {
1110            self.call_count.fetch_add(1, Ordering::SeqCst);
1111            Ok("id".to_string())
1112        }
1113    }
1114
1115    fn test_event<'a>(source: &'a str, bus: &'a str) -> CrossServiceEvent<'a> {
1116        CrossServiceEvent {
1117            source,
1118            detail_type: "type",
1119            detail: "{}",
1120            event_bus: bus,
1121            account_id: "111111111111",
1122            region: "eu-west-1",
1123            resources: &[],
1124            principal_arn: None,
1125        }
1126    }
1127
1128    struct MockKinesis {
1129        call_count: AtomicUsize,
1130    }
1131    impl KinesisDelivery for MockKinesis {
1132        fn put_record(&self, _stream_arn: &str, _data: &str, _partition_key: &str) {
1133            self.call_count.fetch_add(1, Ordering::SeqCst);
1134        }
1135    }
1136
1137    struct MockStepFunctions {
1138        call_count: AtomicUsize,
1139    }
1140    impl StepFunctionsDelivery for MockStepFunctions {
1141        fn start_execution(&self, _state_machine_arn: &str, _input: &str) {
1142            self.call_count.fetch_add(1, Ordering::SeqCst);
1143        }
1144    }
1145
1146    #[test]
1147    fn delivery_bus_new_has_no_senders() {
1148        let bus = DeliveryBus::new();
1149        // Calling methods without senders should be no-ops
1150        bus.send_to_sqs("arn:queue", "body", &HashMap::new());
1151        bus.publish_to_sns("arn:topic", "msg", None);
1152        let _ = bus.put_event_to_eventbridge(&test_event("src", "default"));
1153        bus.send_to_kinesis("arn:stream", "data", "pk");
1154        bus.start_stepfunctions_execution("arn:sfn", "{}");
1155        // No panics = success
1156    }
1157
1158    #[test]
1159    fn delivery_bus_default_is_same_as_new() {
1160        let bus = DeliveryBus::default();
1161        bus.send_to_sqs("arn:q", "b", &HashMap::new());
1162    }
1163
1164    #[test]
1165    fn send_to_sqs_calls_sender() {
1166        let mock = Arc::new(MockSqs {
1167            call_count: AtomicUsize::new(0),
1168        });
1169        let bus = DeliveryBus::new().with_sqs(mock.clone());
1170
1171        bus.send_to_sqs("arn:queue", "msg", &HashMap::new());
1172        assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1173
1174        bus.send_to_sqs("arn:queue2", "msg2", &HashMap::new());
1175        assert_eq!(mock.call_count.load(Ordering::SeqCst), 2);
1176    }
1177
1178    #[test]
1179    fn send_to_sqs_with_attrs_calls_sender() {
1180        let mock = Arc::new(MockSqs {
1181            call_count: AtomicUsize::new(0),
1182        });
1183        let bus = DeliveryBus::new().with_sqs(mock.clone());
1184
1185        let mut attrs = HashMap::new();
1186        attrs.insert(
1187            "key".to_string(),
1188            SqsMessageAttribute {
1189                data_type: "String".to_string(),
1190                string_value: Some("val".to_string()),
1191                binary_value: None,
1192            },
1193        );
1194        bus.send_to_sqs_with_attrs("arn:q", "body", &attrs, Some("group"), Some("dedup"));
1195        assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1196    }
1197
1198    #[test]
1199    fn publish_to_sns_calls_sender() {
1200        let mock = Arc::new(MockSns {
1201            call_count: AtomicUsize::new(0),
1202        });
1203        let bus = DeliveryBus::new().with_sns(mock.clone());
1204
1205        bus.publish_to_sns("arn:topic", "message", Some("subject"));
1206        assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1207    }
1208
1209    #[test]
1210    fn put_event_to_eventbridge_calls_sender() {
1211        let mock = Arc::new(MockEventBridge {
1212            call_count: AtomicUsize::new(0),
1213        });
1214        let bus = DeliveryBus::new().with_eventbridge(mock.clone());
1215
1216        let _ = bus.put_event_to_eventbridge(&test_event("aws.s3", "default"));
1217        assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1218    }
1219
1220    #[test]
1221    fn send_to_kinesis_calls_sender() {
1222        let mock = Arc::new(MockKinesis {
1223            call_count: AtomicUsize::new(0),
1224        });
1225        let bus = DeliveryBus::new().with_kinesis(mock.clone());
1226
1227        bus.send_to_kinesis("arn:stream", "data", "partition-key");
1228        assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1229    }
1230
1231    #[test]
1232    fn start_stepfunctions_calls_sender() {
1233        let mock = Arc::new(MockStepFunctions {
1234            call_count: AtomicUsize::new(0),
1235        });
1236        let bus = DeliveryBus::new().with_stepfunctions(mock.clone());
1237
1238        bus.start_stepfunctions_execution("arn:sfn:machine", r#"{"key":"val"}"#);
1239        assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1240    }
1241
1242    #[test]
1243    fn builder_chaining_works() {
1244        let sqs = Arc::new(MockSqs {
1245            call_count: AtomicUsize::new(0),
1246        });
1247        let sns = Arc::new(MockSns {
1248            call_count: AtomicUsize::new(0),
1249        });
1250        let eb = Arc::new(MockEventBridge {
1251            call_count: AtomicUsize::new(0),
1252        });
1253        let kin = Arc::new(MockKinesis {
1254            call_count: AtomicUsize::new(0),
1255        });
1256        let sfn = Arc::new(MockStepFunctions {
1257            call_count: AtomicUsize::new(0),
1258        });
1259
1260        let bus = DeliveryBus::new()
1261            .with_sqs(sqs.clone())
1262            .with_sns(sns.clone())
1263            .with_eventbridge(eb.clone())
1264            .with_kinesis(kin.clone())
1265            .with_stepfunctions(sfn.clone());
1266
1267        bus.send_to_sqs("q", "m", &HashMap::new());
1268        bus.publish_to_sns("t", "m", None);
1269        let _ = bus.put_event_to_eventbridge(&test_event("s", "b"));
1270        bus.send_to_kinesis("s", "d", "k");
1271        bus.start_stepfunctions_execution("sm", "{}");
1272
1273        assert_eq!(sqs.call_count.load(Ordering::SeqCst), 1);
1274        assert_eq!(sns.call_count.load(Ordering::SeqCst), 1);
1275        assert_eq!(eb.call_count.load(Ordering::SeqCst), 1);
1276        assert_eq!(kin.call_count.load(Ordering::SeqCst), 1);
1277        assert_eq!(sfn.call_count.load(Ordering::SeqCst), 1);
1278    }
1279
1280    #[tokio::test]
1281    async fn invoke_lambda_returns_none_without_invoker() {
1282        let bus = DeliveryBus::new();
1283        let result = bus.invoke_lambda("arn:lambda", "{}").await;
1284        assert!(result.is_none());
1285    }
1286}