Skip to main content

fakecloud_sdk/
client.rs

1use crate::error::Error;
2use crate::types::*;
3use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
4
5/// Client for the fakecloud introspection and simulation API (`/_fakecloud/*`).
6pub struct FakeCloud {
7    base_url: String,
8    client: reqwest::Client,
9}
10
11impl FakeCloud {
12    /// Create a new client pointing at the given fakecloud base URL (e.g. `http://localhost:4566`).
13    pub fn new(base_url: &str) -> Self {
14        Self {
15            base_url: base_url.trim_end_matches('/').to_string(),
16            client: reqwest::Client::new(),
17        }
18    }
19
20    // ── Health & Reset ──────────────────────────────────────────────
21
22    /// Check server health.
23    pub async fn health(&self) -> Result<HealthResponse, Error> {
24        let resp = self
25            .client
26            .get(format!("{}/_fakecloud/health", self.base_url))
27            .send()
28            .await?;
29        Self::parse(resp).await
30    }
31
32    /// Reset all service state. Uses the legacy `/_reset` endpoint.
33    pub async fn reset(&self) -> Result<ResetResponse, Error> {
34        let resp = self
35            .client
36            .post(format!("{}/_reset", self.base_url))
37            .send()
38            .await?;
39        Self::parse(resp).await
40    }
41
42    /// Create an IAM admin user in a specific account. Returns credentials
43    /// for the new user. Solves the multi-account bootstrap problem: the
44    /// root bypass only targets the default account, so this endpoint lets
45    /// callers create credentials for any account.
46    pub async fn create_admin(
47        &self,
48        account_id: &str,
49        user_name: &str,
50    ) -> Result<CreateAdminResponse, Error> {
51        let resp = self
52            .client
53            .post(format!("{}/_fakecloud/iam/create-admin", self.base_url))
54            .json(&CreateAdminRequest {
55                account_id: account_id.to_string(),
56                user_name: user_name.to_string(),
57            })
58            .send()
59            .await?;
60        Self::parse(resp).await
61    }
62
63    /// Reset a single service's state.
64    pub async fn reset_service(&self, service: &str) -> Result<ResetServiceResponse, Error> {
65        let resp = self
66            .client
67            .post(format!("{}/_fakecloud/reset/{}", self.base_url, service))
68            .send()
69            .await?;
70        Self::parse(resp).await
71    }
72
73    /// Reset a single service's state for a specific account only.
74    pub async fn reset_service_for_account(
75        &self,
76        service: &str,
77        account_id: &str,
78    ) -> Result<ResetServiceResponse, Error> {
79        let resp = self
80            .client
81            .post(format!(
82                "{}/_fakecloud/reset/{}/{}",
83                self.base_url, service, account_id
84            ))
85            .send()
86            .await?;
87        Self::parse(resp).await
88    }
89
90    // ── Sub-clients ─────────────────────────────────────────────────
91
92    pub fn lambda(&self) -> LambdaClient<'_> {
93        LambdaClient { fc: self }
94    }
95
96    pub fn ses(&self) -> SesClient<'_> {
97        SesClient { fc: self }
98    }
99
100    pub fn sns(&self) -> SnsClient<'_> {
101        SnsClient { fc: self }
102    }
103
104    pub fn sqs(&self) -> SqsClient<'_> {
105        SqsClient { fc: self }
106    }
107
108    pub fn events(&self) -> EventsClient<'_> {
109        EventsClient { fc: self }
110    }
111
112    pub fn s3(&self) -> S3Client<'_> {
113        S3Client { fc: self }
114    }
115
116    pub fn dynamodb(&self) -> DynamoDbClient<'_> {
117        DynamoDbClient { fc: self }
118    }
119
120    pub fn secretsmanager(&self) -> SecretsManagerClient<'_> {
121        SecretsManagerClient { fc: self }
122    }
123
124    pub fn cognito(&self) -> CognitoClient<'_> {
125        CognitoClient { fc: self }
126    }
127
128    pub fn rds(&self) -> RdsClient<'_> {
129        RdsClient { fc: self }
130    }
131
132    pub fn elasticache(&self) -> ElastiCacheClient<'_> {
133        ElastiCacheClient { fc: self }
134    }
135
136    pub fn apigatewayv2(&self) -> ApiGatewayV2Client<'_> {
137        ApiGatewayV2Client { fc: self }
138    }
139
140    pub fn stepfunctions(&self) -> StepFunctionsClient<'_> {
141        StepFunctionsClient { fc: self }
142    }
143
144    pub fn bedrock(&self) -> BedrockClient<'_> {
145        BedrockClient { fc: self }
146    }
147
148    pub fn bedrock_agent(&self) -> BedrockAgentClient<'_> {
149        BedrockAgentClient { fc: self }
150    }
151
152    pub fn bedrock_agent_runtime(&self) -> BedrockAgentRuntimeClient<'_> {
153        BedrockAgentRuntimeClient { fc: self }
154    }
155
156    pub fn ecs(&self) -> EcsClient<'_> {
157        EcsClient { fc: self }
158    }
159
160    pub fn application_autoscaling(&self) -> ApplicationAutoScalingClient<'_> {
161        ApplicationAutoScalingClient { fc: self }
162    }
163
164    pub fn athena(&self) -> AthenaClient<'_> {
165        AthenaClient { fc: self }
166    }
167
168    pub fn organizations(&self) -> OrganizationsClient<'_> {
169        OrganizationsClient { fc: self }
170    }
171
172    pub fn acm(&self) -> AcmClient<'_> {
173        AcmClient { fc: self }
174    }
175
176    pub fn ecr(&self) -> EcrClient<'_> {
177        EcrClient { fc: self }
178    }
179
180    pub fn elbv2(&self) -> Elbv2Client<'_> {
181        Elbv2Client { fc: self }
182    }
183
184    pub fn glue(&self) -> GlueClient<'_> {
185        GlueClient { fc: self }
186    }
187
188    pub fn cloudwatch(&self) -> CloudWatchClient<'_> {
189        CloudWatchClient { fc: self }
190    }
191
192    pub fn firehose(&self) -> FirehoseClient<'_> {
193        FirehoseClient { fc: self }
194    }
195
196    pub fn logs(&self) -> LogsClient<'_> {
197        LogsClient { fc: self }
198    }
199
200    pub fn route53(&self) -> Route53Client<'_> {
201        Route53Client { fc: self }
202    }
203
204    pub fn scheduler(&self) -> SchedulerClient<'_> {
205        SchedulerClient { fc: self }
206    }
207
208    pub fn ssm(&self) -> SsmClient<'_> {
209        SsmClient { fc: self }
210    }
211
212    pub fn kms(&self) -> KmsClient<'_> {
213        KmsClient { fc: self }
214    }
215
216    pub fn wafv2(&self) -> WafV2Client<'_> {
217        WafV2Client { fc: self }
218    }
219
220    pub fn cloudfront(&self) -> CloudFrontClient<'_> {
221        CloudFrontClient { fc: self }
222    }
223
224    // ── Internal helpers ────────────────────────────────────────────
225
226    async fn parse<T: serde::de::DeserializeOwned>(resp: reqwest::Response) -> Result<T, Error> {
227        let status = resp.status().as_u16();
228        if !resp.status().is_success() {
229            let body = resp.text().await.unwrap_or_default();
230            return Err(Error::Api { status, body });
231        }
232        Ok(resp.json::<T>().await?)
233    }
234}
235
236// ── RDS ─────────────────────────────────────────────────────────────
237
238pub struct RdsClient<'a> {
239    fc: &'a FakeCloud,
240}
241
242impl RdsClient<'_> {
243    /// List fakecloud-managed RDS DB instances with runtime metadata.
244    pub async fn get_instances(&self) -> Result<RdsInstancesResponse, Error> {
245        let resp = self
246            .fc
247            .client
248            .get(format!("{}/_fakecloud/rds/instances", self.fc.base_url))
249            .send()
250            .await?;
251        FakeCloud::parse(resp).await
252    }
253
254    /// Bridge endpoint used by the PostgreSQL `aws_lambda` extension
255    /// running inside an RDS container to invoke a Lambda function. A
256    /// `status_code` of `502` is returned (with the response body still
257    /// JSON-decodable) when the target Lambda is unavailable.
258    pub async fn lambda_invoke(
259        &self,
260        req: &RdsLambdaInvokeRequest,
261    ) -> Result<RdsLambdaInvokeResponse, Error> {
262        let resp = self
263            .fc
264            .client
265            .post(format!("{}/_fakecloud/rds/lambda-invoke", self.fc.base_url))
266            .json(req)
267            .send()
268            .await?;
269        let status = resp.status().as_u16();
270        // 502 is a valid bridge response (SERVICE_UNAVAILABLE) — body is
271        // still a well-formed RdsLambdaInvokeResponse, so parse it
272        // rather than treating the call as a transport error.
273        if status == 502 || resp.status().is_success() {
274            return Ok(resp.json::<RdsLambdaInvokeResponse>().await?);
275        }
276        let body = resp.text().await.unwrap_or_default();
277        Err(Error::Api { status, body })
278    }
279
280    /// Bridge endpoint for the PostgreSQL `aws_s3` extension's import
281    /// path. Mirrors `aws_s3.table_import_from_s3` at the transport layer.
282    pub async fn s3_import(&self, req: &RdsS3ImportRequest) -> Result<RdsS3ImportResponse, Error> {
283        let resp = self
284            .fc
285            .client
286            .post(format!("{}/_fakecloud/rds/s3-import", self.fc.base_url))
287            .json(req)
288            .send()
289            .await?;
290        FakeCloud::parse(resp).await
291    }
292
293    /// Bridge endpoint for the PostgreSQL `aws_s3` extension's export
294    /// path. Mirrors `aws_s3.query_export_to_s3` at the transport layer.
295    pub async fn s3_export(&self, req: &RdsS3ExportRequest) -> Result<RdsS3ExportResponse, Error> {
296        let resp = self
297            .fc
298            .client
299            .post(format!("{}/_fakecloud/rds/s3-export", self.fc.base_url))
300            .json(req)
301            .send()
302            .await?;
303        FakeCloud::parse(resp).await
304    }
305}
306
307// ── ElastiCache ─────────────────────────────────────────────────────
308
309pub struct ElastiCacheClient<'a> {
310    fc: &'a FakeCloud,
311}
312
313impl ElastiCacheClient<'_> {
314    /// List fakecloud-managed ElastiCache cache clusters with runtime metadata.
315    pub async fn get_clusters(&self) -> Result<ElastiCacheClustersResponse, Error> {
316        let resp = self
317            .fc
318            .client
319            .get(format!(
320                "{}/_fakecloud/elasticache/clusters",
321                self.fc.base_url
322            ))
323            .send()
324            .await?;
325        FakeCloud::parse(resp).await
326    }
327
328    /// List fakecloud-managed ElastiCache replication groups with runtime metadata.
329    pub async fn get_replication_groups(
330        &self,
331    ) -> Result<ElastiCacheReplicationGroupsResponse, Error> {
332        let resp = self
333            .fc
334            .client
335            .get(format!(
336                "{}/_fakecloud/elasticache/replication-groups",
337                self.fc.base_url
338            ))
339            .send()
340            .await?;
341        FakeCloud::parse(resp).await
342    }
343
344    /// List fakecloud-managed ElastiCache serverless caches with runtime metadata.
345    pub async fn get_serverless_caches(
346        &self,
347    ) -> Result<ElastiCacheServerlessCachesResponse, Error> {
348        let resp = self
349            .fc
350            .client
351            .get(format!(
352                "{}/_fakecloud/elasticache/serverless-caches",
353                self.fc.base_url
354            ))
355            .send()
356            .await?;
357        FakeCloud::parse(resp).await
358    }
359
360    /// List ACL state (users + user groups) for ElastiCache replication groups
361    /// that have one or more user groups attached.
362    pub async fn get_acls(&self) -> Result<ElastiCacheAclsResponse, Error> {
363        let resp = self
364            .fc
365            .client
366            .get(format!("{}/_fakecloud/elasticache/acls", self.fc.base_url))
367            .send()
368            .await?;
369        FakeCloud::parse(resp).await
370    }
371}
372
373// ── Lambda ──────────────────────────────────────────────────────────
374
375pub struct LambdaClient<'a> {
376    fc: &'a FakeCloud,
377}
378
379impl LambdaClient<'_> {
380    /// List recorded Lambda invocations.
381    pub async fn get_invocations(&self) -> Result<LambdaInvocationsResponse, Error> {
382        let resp = self
383            .fc
384            .client
385            .get(format!(
386                "{}/_fakecloud/lambda/invocations",
387                self.fc.base_url
388            ))
389            .send()
390            .await?;
391        FakeCloud::parse(resp).await
392    }
393
394    /// List warm (cached) Lambda containers.
395    pub async fn get_warm_containers(&self) -> Result<WarmContainersResponse, Error> {
396        let resp = self
397            .fc
398            .client
399            .get(format!(
400                "{}/_fakecloud/lambda/warm-containers",
401                self.fc.base_url
402            ))
403            .send()
404            .await?;
405        FakeCloud::parse(resp).await
406    }
407
408    /// Evict the warm container for a specific function.
409    pub async fn evict_container(
410        &self,
411        function_name: &str,
412    ) -> Result<EvictContainerResponse, Error> {
413        let resp = self
414            .fc
415            .client
416            .post(format!(
417                "{}/_fakecloud/lambda/{}/evict-container",
418                self.fc.base_url, function_name
419            ))
420            .send()
421            .await?;
422        FakeCloud::parse(resp).await
423    }
424
425    /// Download the raw zip bytes for a function's code. Pass `"latest"`
426    /// for the live function code, or a published version string (e.g.
427    /// `"1"`) for a version snapshot.
428    pub async fn download_function_code(
429        &self,
430        account_id: &str,
431        function_name: &str,
432        qualifier_or_latest: &str,
433    ) -> Result<Vec<u8>, Error> {
434        let resp = self
435            .fc
436            .client
437            .get(format!(
438                "{}/_fakecloud/lambda/function-code/{}/{}/{}.zip",
439                self.fc.base_url, account_id, function_name, qualifier_or_latest
440            ))
441            .send()
442            .await?;
443        let status = resp.status().as_u16();
444        if !resp.status().is_success() {
445            let body = resp.text().await.unwrap_or_default();
446            return Err(Error::Api { status, body });
447        }
448        Ok(resp.bytes().await?.to_vec())
449    }
450
451    /// Download the raw zip bytes for a published Lambda layer version.
452    pub async fn download_layer_content(
453        &self,
454        account_id: &str,
455        layer_name: &str,
456        version: i64,
457    ) -> Result<Vec<u8>, Error> {
458        let resp = self
459            .fc
460            .client
461            .get(format!(
462                "{}/_fakecloud/lambda/layer-content/{}/{}/{}.zip",
463                self.fc.base_url, account_id, layer_name, version
464            ))
465            .send()
466            .await?;
467        let status = resp.status().as_u16();
468        if !resp.status().is_success() {
469            let body = resp.text().await.unwrap_or_default();
470            return Err(Error::Api { status, body });
471        }
472        Ok(resp.bytes().await?.to_vec())
473    }
474}
475
476// ── SES ─────────────────────────────────────────────────────────────
477
478pub struct SesClient<'a> {
479    fc: &'a FakeCloud,
480}
481
482impl SesClient<'_> {
483    /// List all sent emails.
484    pub async fn get_emails(&self) -> Result<SesEmailsResponse, Error> {
485        let resp = self
486            .fc
487            .client
488            .get(format!("{}/_fakecloud/ses/emails", self.fc.base_url))
489            .send()
490            .await?;
491        FakeCloud::parse(resp).await
492    }
493
494    /// Simulate an inbound email (SES receipt rules).
495    pub async fn simulate_inbound(
496        &self,
497        req: &InboundEmailRequest,
498    ) -> Result<InboundEmailResponse, Error> {
499        let resp = self
500            .fc
501            .client
502            .post(format!("{}/_fakecloud/ses/inbound", self.fc.base_url))
503            .json(req)
504            .send()
505            .await?;
506        FakeCloud::parse(resp).await
507    }
508
509    /// Snapshot the running SES counters (currently only
510    /// `suppressed_drops_total`).
511    pub async fn get_metrics(&self) -> Result<SesMetricsResponse, Error> {
512        let resp = self
513            .fc
514            .client
515            .get(format!("{}/_fakecloud/ses/metrics", self.fc.base_url))
516            .send()
517            .await?;
518        FakeCloud::parse(resp).await
519    }
520
521    /// List recorded SES bounces with per-recipient bounce metadata.
522    pub async fn get_bounces(&self) -> Result<SesBouncesResponse, Error> {
523        let resp = self
524            .fc
525            .client
526            .get(format!("{}/_fakecloud/ses/bounces", self.fc.base_url))
527            .send()
528            .await?;
529        FakeCloud::parse(resp).await
530    }
531
532    /// Flip the SES account-level `production_access_enabled` flag.
533    /// `sandbox=true` puts the account back into sandbox mode (production
534    /// access disabled); `sandbox=false` re-enables production access.
535    pub async fn set_sandbox(&self, sandbox: bool) -> Result<SesSandboxResponse, Error> {
536        let resp = self
537            .fc
538            .client
539            .post(format!(
540                "{}/_fakecloud/ses/account/sandbox",
541                self.fc.base_url
542            ))
543            .json(&SesSandboxRequest { sandbox })
544            .send()
545            .await?;
546        FakeCloud::parse(resp).await
547    }
548
549    /// List event-destination delivery dispatches recorded by the
550    /// SES sender (one row per dispatched event-destination target).
551    pub async fn get_event_destination_deliveries(
552        &self,
553    ) -> Result<SesEventDestinationDeliveriesResponse, Error> {
554        let resp = self
555            .fc
556            .client
557            .get(format!(
558                "{}/_fakecloud/ses/event-destinations/deliveries",
559                self.fc.base_url
560            ))
561            .send()
562            .await?;
563        FakeCloud::parse(resp).await
564    }
565
566    /// Get the deterministic DKIM public key + selector + signing-enabled
567    /// flag for an identity. 404 if the identity is unknown.
568    pub async fn get_dkim_public_key(
569        &self,
570        identity: &str,
571    ) -> Result<SesDkimPublicKeyResponse, Error> {
572        let resp = self
573            .fc
574            .client
575            .get(format!(
576                "{}/_fakecloud/ses/identities/{}/dkim-public-key",
577                self.fc.base_url, identity
578            ))
579            .send()
580            .await?;
581        FakeCloud::parse(resp).await
582    }
583
584    /// Flip an identity's `MailFromDomainStatus`. Must be one of
585    /// `NotStarted` / `Pending` / `Success` / `Failed`.
586    pub async fn set_mail_from_status(
587        &self,
588        identity: &str,
589        status: &str,
590    ) -> Result<SesMailFromStatusResponse, Error> {
591        let resp = self
592            .fc
593            .client
594            .post(format!(
595                "{}/_fakecloud/ses/identities/{}/mail-from-status",
596                self.fc.base_url, identity
597            ))
598            .json(&SesMailFromStatusRequest {
599                status: status.to_string(),
600            })
601            .send()
602            .await?;
603        FakeCloud::parse(resp).await
604    }
605
606    /// Get a per-message insights snapshot (sends, deliveries, bounces,
607    /// complaints, ...). 404 if the message id is unknown.
608    pub async fn get_message_insights(
609        &self,
610        message_id: &str,
611    ) -> Result<SesMessageInsightsResponse, Error> {
612        let resp = self
613            .fc
614            .client
615            .get(format!(
616                "{}/_fakecloud/ses/messages/{}/insights",
617                self.fc.base_url, message_id
618            ))
619            .send()
620            .await?;
621        FakeCloud::parse(resp).await
622    }
623
624    /// List submissions received via the SES SMTP endpoint.
625    pub async fn get_smtp_submissions(&self) -> Result<SesSmtpSubmissionsResponse, Error> {
626        let resp = self
627            .fc
628            .client
629            .get(format!(
630                "{}/_fakecloud/ses/smtp/submissions",
631                self.fc.base_url
632            ))
633            .send()
634            .await?;
635        FakeCloud::parse(resp).await
636    }
637}
638
639// ── SNS ─────────────────────────────────────────────────────────────
640
641pub struct SnsClient<'a> {
642    fc: &'a FakeCloud,
643}
644
645impl SnsClient<'_> {
646    /// List all published SNS messages.
647    pub async fn get_messages(&self) -> Result<SnsMessagesResponse, Error> {
648        let resp = self
649            .fc
650            .client
651            .get(format!("{}/_fakecloud/sns/messages", self.fc.base_url))
652            .send()
653            .await?;
654        FakeCloud::parse(resp).await
655    }
656
657    /// List subscriptions pending confirmation.
658    pub async fn get_pending_confirmations(&self) -> Result<PendingConfirmationsResponse, Error> {
659        let resp = self
660            .fc
661            .client
662            .get(format!(
663                "{}/_fakecloud/sns/pending-confirmations",
664                self.fc.base_url
665            ))
666            .send()
667            .await?;
668        FakeCloud::parse(resp).await
669    }
670
671    /// Confirm a pending subscription.
672    pub async fn confirm_subscription(
673        &self,
674        req: &ConfirmSubscriptionRequest,
675    ) -> Result<ConfirmSubscriptionResponse, Error> {
676        let resp = self
677            .fc
678            .client
679            .post(format!(
680                "{}/_fakecloud/sns/confirm-subscription",
681                self.fc.base_url
682            ))
683            .json(req)
684            .send()
685            .await?;
686        FakeCloud::parse(resp).await
687    }
688
689    /// Fetch the PEM-encoded SNS signing certificate used to sign
690    /// outbound notification payloads. Served as
691    /// `application/x-pem-file`; returned as the raw PEM string so
692    /// callers can hand it straight to an X.509 parser.
693    pub async fn cert_pem(&self) -> Result<String, Error> {
694        let resp = self
695            .fc
696            .client
697            .get(format!("{}/_fakecloud/sns/cert.pem", self.fc.base_url))
698            .send()
699            .await?;
700        let status = resp.status().as_u16();
701        if !resp.status().is_success() {
702            let body = resp.text().await.unwrap_or_default();
703            return Err(Error::Api { status, body });
704        }
705        Ok(resp.text().await?)
706    }
707
708    /// List recorded SMS publications (phone number + message body).
709    pub async fn sms(&self) -> Result<SnsSmsResponse, Error> {
710        let resp = self
711            .fc
712            .client
713            .get(format!("{}/_fakecloud/sns/sms", self.fc.base_url))
714            .send()
715            .await?;
716        FakeCloud::parse(resp).await
717    }
718}
719
720// ── SQS ─────────────────────────────────────────────────────────────
721
722pub struct SqsClient<'a> {
723    fc: &'a FakeCloud,
724}
725
726impl SqsClient<'_> {
727    /// List all messages across all queues.
728    pub async fn get_messages(&self) -> Result<SqsMessagesResponse, Error> {
729        let resp = self
730            .fc
731            .client
732            .get(format!("{}/_fakecloud/sqs/messages", self.fc.base_url))
733            .send()
734            .await?;
735        FakeCloud::parse(resp).await
736    }
737
738    /// Tick the message expiration processor (expire visibility-timed-out messages).
739    pub async fn tick_expiration(&self) -> Result<ExpirationTickResponse, Error> {
740        let resp = self
741            .fc
742            .client
743            .post(format!(
744                "{}/_fakecloud/sqs/expiration-processor/tick",
745                self.fc.base_url
746            ))
747            .send()
748            .await?;
749        FakeCloud::parse(resp).await
750    }
751
752    /// Force all messages in a queue to its DLQ.
753    pub async fn force_dlq(&self, queue_name: &str) -> Result<ForceDlqResponse, Error> {
754        let resp = self
755            .fc
756            .client
757            .post(format!(
758                "{}/_fakecloud/sqs/{}/force-dlq",
759                self.fc.base_url, queue_name
760            ))
761            .send()
762            .await?;
763        FakeCloud::parse(resp).await
764    }
765}
766
767// ── Application Auto Scaling ────────────────────────────────────────
768
769pub struct ApplicationAutoScalingClient<'a> {
770    fc: &'a FakeCloud,
771}
772
773impl ApplicationAutoScalingClient<'_> {
774    /// Force the watcher to evaluate every scaling policy now. Returns
775    /// the number of policies that applied a capacity change on this
776    /// tick. Useful in tests so callers don't have to wait for the
777    /// wall-clock 15s interval.
778    pub async fn tick(&self) -> Result<AppAsTickResponse, Error> {
779        let resp = self
780            .fc
781            .client
782            .post(format!(
783                "{}/_fakecloud/application-autoscaling/tick",
784                self.fc.base_url
785            ))
786            .send()
787            .await?;
788        FakeCloud::parse(resp).await
789    }
790
791    /// Force the scheduled-action executor to evaluate every
792    /// `ScheduledAction` now. Returns the number of actions that
793    /// fired this tick. Useful in tests so callers don't have to wait
794    /// for the wall-clock 30s interval.
795    pub async fn scheduled_tick(&self) -> Result<AppAsScheduledTickResponse, Error> {
796        let resp = self
797            .fc
798            .client
799            .post(format!(
800                "{}/_fakecloud/application-autoscaling/scheduled-tick",
801                self.fc.base_url
802            ))
803            .send()
804            .await?;
805        FakeCloud::parse(resp).await
806    }
807}
808
809// ── EventBridge ─────────────────────────────────────────────────────
810
811pub struct EventsClient<'a> {
812    fc: &'a FakeCloud,
813}
814
815impl EventsClient<'_> {
816    /// Get event history and delivery records.
817    pub async fn get_history(&self) -> Result<EventHistoryResponse, Error> {
818        let resp = self
819            .fc
820            .client
821            .get(format!("{}/_fakecloud/events/history", self.fc.base_url))
822            .send()
823            .await?;
824        FakeCloud::parse(resp).await
825    }
826
827    /// Fire a specific EventBridge rule manually.
828    pub async fn fire_rule(&self, req: &FireRuleRequest) -> Result<FireRuleResponse, Error> {
829        let resp = self
830            .fc
831            .client
832            .post(format!("{}/_fakecloud/events/fire-rule", self.fc.base_url))
833            .json(req)
834            .send()
835            .await?;
836        FakeCloud::parse(resp).await
837    }
838}
839
840// ── S3 ──────────────────────────────────────────────────────────────
841
842pub struct S3Client<'a> {
843    fc: &'a FakeCloud,
844}
845
846impl S3Client<'_> {
847    /// List S3 notification events.
848    pub async fn get_notifications(&self) -> Result<S3NotificationsResponse, Error> {
849        let resp = self
850            .fc
851            .client
852            .get(format!("{}/_fakecloud/s3/notifications", self.fc.base_url))
853            .send()
854            .await?;
855        FakeCloud::parse(resp).await
856    }
857
858    /// Tick the S3 lifecycle processor.
859    pub async fn tick_lifecycle(&self) -> Result<LifecycleTickResponse, Error> {
860        let resp = self
861            .fc
862            .client
863            .post(format!(
864                "{}/_fakecloud/s3/lifecycle-processor/tick",
865                self.fc.base_url
866            ))
867            .send()
868            .await?;
869        FakeCloud::parse(resp).await
870    }
871
872    /// List S3 access points across all accounts.
873    pub async fn get_access_points(&self) -> Result<S3AccessPointsResponse, Error> {
874        let resp = self
875            .fc
876            .client
877            .get(format!("{}/_fakecloud/s3/access-points", self.fc.base_url))
878            .send()
879            .await?;
880        FakeCloud::parse(resp).await
881    }
882
883    /// List stored WriteGetObjectResponse bodies (S3 Object Lambda).
884    pub async fn get_object_lambda_responses(
885        &self,
886    ) -> Result<S3ObjectLambdaResponsesResponse, Error> {
887        let resp = self
888            .fc
889            .client
890            .get(format!(
891                "{}/_fakecloud/s3/object-lambda-responses",
892                self.fc.base_url
893            ))
894            .send()
895            .await?;
896        FakeCloud::parse(resp).await
897    }
898}
899
900// ── DynamoDB ────────────────────────────────────────────────────────
901
902pub struct DynamoDbClient<'a> {
903    fc: &'a FakeCloud,
904}
905
906impl DynamoDbClient<'_> {
907    /// Tick the DynamoDB TTL processor.
908    pub async fn tick_ttl(&self) -> Result<TtlTickResponse, Error> {
909        let resp = self
910            .fc
911            .client
912            .post(format!(
913                "{}/_fakecloud/dynamodb/ttl-processor/tick",
914                self.fc.base_url
915            ))
916            .send()
917            .await?;
918        FakeCloud::parse(resp).await
919    }
920}
921
922// ── SecretsManager ──────────────────────────────────────────────────
923
924pub struct SecretsManagerClient<'a> {
925    fc: &'a FakeCloud,
926}
927
928impl SecretsManagerClient<'_> {
929    /// Tick the SecretsManager rotation scheduler.
930    pub async fn tick_rotation(&self) -> Result<RotationTickResponse, Error> {
931        let resp = self
932            .fc
933            .client
934            .post(format!(
935                "{}/_fakecloud/secretsmanager/rotation-scheduler/tick",
936                self.fc.base_url
937            ))
938            .send()
939            .await?;
940        FakeCloud::parse(resp).await
941    }
942}
943
944// ── Cognito ─────────────────────────────────────────────────────────
945
946pub struct CognitoClient<'a> {
947    fc: &'a FakeCloud,
948}
949
950impl CognitoClient<'_> {
951    /// Get confirmation codes for a specific user.
952    pub async fn get_user_codes(
953        &self,
954        pool_id: &str,
955        username: &str,
956    ) -> Result<UserConfirmationCodes, Error> {
957        let resp = self
958            .fc
959            .client
960            .get(format!(
961                "{}/_fakecloud/cognito/confirmation-codes/{}/{}",
962                self.fc.base_url, pool_id, username
963            ))
964            .send()
965            .await?;
966        FakeCloud::parse(resp).await
967    }
968
969    /// List all confirmation codes across all pools.
970    pub async fn get_confirmation_codes(&self) -> Result<ConfirmationCodesResponse, Error> {
971        let resp = self
972            .fc
973            .client
974            .get(format!(
975                "{}/_fakecloud/cognito/confirmation-codes",
976                self.fc.base_url
977            ))
978            .send()
979            .await?;
980        FakeCloud::parse(resp).await
981    }
982
983    /// Confirm a user (bypass email/phone verification).
984    pub async fn confirm_user(
985        &self,
986        req: &ConfirmUserRequest,
987    ) -> Result<ConfirmUserResponse, Error> {
988        let resp = self
989            .fc
990            .client
991            .post(format!(
992                "{}/_fakecloud/cognito/confirm-user",
993                self.fc.base_url
994            ))
995            .json(req)
996            .send()
997            .await?;
998        let status = resp.status().as_u16();
999        let body: ConfirmUserResponse = resp.json().await?;
1000        if status >= 400 {
1001            return Err(Error::Api {
1002                status,
1003                body: body.error.unwrap_or_default(),
1004            });
1005        }
1006        Ok(body)
1007    }
1008
1009    /// List all active tokens.
1010    pub async fn get_tokens(&self) -> Result<TokensResponse, Error> {
1011        let resp = self
1012            .fc
1013            .client
1014            .get(format!("{}/_fakecloud/cognito/tokens", self.fc.base_url))
1015            .send()
1016            .await?;
1017        FakeCloud::parse(resp).await
1018    }
1019
1020    /// Expire tokens (optionally filtered by pool/user).
1021    pub async fn expire_tokens(
1022        &self,
1023        req: &ExpireTokensRequest,
1024    ) -> Result<ExpireTokensResponse, Error> {
1025        let resp = self
1026            .fc
1027            .client
1028            .post(format!(
1029                "{}/_fakecloud/cognito/expire-tokens",
1030                self.fc.base_url
1031            ))
1032            .json(req)
1033            .send()
1034            .await?;
1035        FakeCloud::parse(resp).await
1036    }
1037
1038    /// List auth events.
1039    pub async fn get_auth_events(&self) -> Result<AuthEventsResponse, Error> {
1040        let resp = self
1041            .fc
1042            .client
1043            .get(format!(
1044                "{}/_fakecloud/cognito/auth-events",
1045                self.fc.base_url
1046            ))
1047            .send()
1048            .await?;
1049        FakeCloud::parse(resp).await
1050    }
1051
1052    /// List PreTokenGeneration Lambda trigger invocations recorded
1053    /// during `InitiateAuth`. Each entry includes the full request /
1054    /// response payloads plus pre-parsed `claims_added`,
1055    /// `claims_overridden`, and `group_overrides` so tests can assert
1056    /// the claim mutation flow without inspecting the issued JWT.
1057    pub async fn get_pre_token_gen_invocations(
1058        &self,
1059    ) -> Result<PreTokenGenInvocationsResponse, Error> {
1060        let resp = self
1061            .fc
1062            .client
1063            .get(format!(
1064                "{}/_fakecloud/cognito/pretokengen/invocations",
1065                self.fc.base_url
1066            ))
1067            .send()
1068            .await?;
1069        FakeCloud::parse(resp).await
1070    }
1071
1072    /// Mint an OAuth2 `authorization_code` for the given `(client_id,
1073    /// redirect_uri, scopes, PKCE)` binding. Lets tests drive the
1074    /// `authorization_code` grant before the hosted-UI lands.
1075    pub async fn mint_authorization_code(
1076        &self,
1077        req: &MintAuthorizationCodeRequest,
1078    ) -> Result<MintAuthorizationCodeResponse, Error> {
1079        let resp = self
1080            .fc
1081            .client
1082            .post(format!(
1083                "{}/_fakecloud/cognito/authorization-codes",
1084                self.fc.base_url
1085            ))
1086            .json(req)
1087            .send()
1088            .await?;
1089        FakeCloud::parse(resp).await
1090    }
1091
1092    /// Register one or more plaintext passwords with the compromised-
1093    /// credentials set so subsequent `InitiateAuth` /
1094    /// `AdminInitiateAuth` calls trip the `BLOCK` action when the pool's
1095    /// `CompromisedCredentialsRiskConfiguration` is enabled.
1096    pub async fn set_compromised_passwords(
1097        &self,
1098        req: &CognitoCompromisedPasswordsRequest,
1099    ) -> Result<CompromisedPasswordsResponse, Error> {
1100        let resp = self
1101            .fc
1102            .client
1103            .post(format!(
1104                "{}/_fakecloud/cognito/compromised-passwords",
1105                self.fc.base_url
1106            ))
1107            .json(req)
1108            .send()
1109            .await?;
1110        FakeCloud::parse(resp).await
1111    }
1112
1113    /// List every registered WebAuthn credential across pools.
1114    pub async fn get_webauthn_credentials(&self) -> Result<WebAuthnCredentialsResponse, Error> {
1115        let resp = self
1116            .fc
1117            .client
1118            .get(format!(
1119                "{}/_fakecloud/cognito/webauthn-credentials",
1120                self.fc.base_url
1121            ))
1122            .send()
1123            .await?;
1124        FakeCloud::parse(resp).await
1125    }
1126}
1127
1128// ── API Gateway v2 ──────────────────────────────────────────────────
1129
1130pub struct ApiGatewayV2Client<'a> {
1131    fc: &'a FakeCloud,
1132}
1133
1134impl ApiGatewayV2Client<'_> {
1135    /// List all HTTP API requests that were received and processed.
1136    pub async fn get_requests(&self) -> Result<ApiGatewayV2RequestsResponse, Error> {
1137        let resp = self
1138            .fc
1139            .client
1140            .get(format!(
1141                "{}/_fakecloud/apigatewayv2/requests",
1142                self.fc.base_url
1143            ))
1144            .send()
1145            .await?;
1146        FakeCloud::parse(resp).await
1147    }
1148
1149    /// List currently-active WebSocket connections across all WebSocket
1150    /// APIs the server has seen.
1151    pub async fn connections(&self) -> Result<ApiGatewayV2ConnectionsResponse, Error> {
1152        let resp = self
1153            .fc
1154            .client
1155            .get(format!(
1156                "{}/_fakecloud/apigatewayv2/connections",
1157                self.fc.base_url
1158            ))
1159            .send()
1160            .await?;
1161        FakeCloud::parse(resp).await
1162    }
1163
1164    /// Fetch the mTLS configuration recorded for a custom domain name
1165    /// (truststore bundle, version, validity). Pass-through JSON — the
1166    /// shape mirrors what the server emits unmodified.
1167    pub async fn mtls_info(&self, name: &str) -> Result<serde_json::Value, Error> {
1168        let resp = self
1169            .fc
1170            .client
1171            .get(format!(
1172                "{}/_fakecloud/apigatewayv2/domain-names/{}/mtls-info",
1173                self.fc.base_url, name
1174            ))
1175            .send()
1176            .await?;
1177        FakeCloud::parse(resp).await
1178    }
1179
1180    /// Build the WebSocket upgrade URL for `api_id`. The SDK doesn't
1181    /// open the socket itself — pair this with `tokio-tungstenite` or
1182    /// any other WebSocket client in test code.
1183    pub fn ws_url(&self, api_id: &str, stage: Option<&str>) -> String {
1184        let base = self
1185            .fc
1186            .base_url
1187            .replacen("https://", "wss://", 1)
1188            .replacen("http://", "ws://", 1);
1189        // API Gateway v2 API IDs are 10-char alphanumeric so encoding is
1190        // a no-op, but defend against future changes by passing through
1191        // reqwest's URL builder for the query parameter.
1192        let url = reqwest::Url::parse(&format!("{}/_fakecloud/apigatewayv2/ws/{}", base, api_id))
1193            .expect("base url + api id form a valid URL");
1194        match stage {
1195            Some(s) => {
1196                let mut u = url;
1197                u.query_pairs_mut().append_pair("stage", s);
1198                u.to_string()
1199            }
1200            None => url.to_string(),
1201        }
1202    }
1203}
1204
1205// ── Step Functions ──────────────────────────────────────────────────
1206
1207pub struct StepFunctionsClient<'a> {
1208    fc: &'a FakeCloud,
1209}
1210
1211impl StepFunctionsClient<'_> {
1212    /// List all Step Functions executions with status, input, output, and timestamps.
1213    pub async fn get_executions(&self) -> Result<StepFunctionsExecutionsResponse, Error> {
1214        let resp = self
1215            .fc
1216            .client
1217            .get(format!(
1218                "{}/_fakecloud/stepfunctions/executions",
1219                self.fc.base_url
1220            ))
1221            .send()
1222            .await?;
1223        FakeCloud::parse(resp).await
1224    }
1225
1226    /// List `StartSyncExecution` invocations with billing details. EXPRESS state
1227    /// machines only — async (`StartExecution`) calls show up under
1228    /// [`Self::get_executions`] instead.
1229    pub async fn get_sync_executions(&self) -> Result<StepFunctionsSyncExecutionsResponse, Error> {
1230        let resp = self
1231            .fc
1232            .client
1233            .get(format!(
1234                "{}/_fakecloud/stepfunctions/sync-executions",
1235                self.fc.base_url
1236            ))
1237            .send()
1238            .await?;
1239        FakeCloud::parse(resp).await
1240    }
1241
1242    /// Return the nested call tree rooted at `execution_arn`. Children are
1243    /// executions that were started by their parent via
1244    /// `arn:aws:states:::states:startExecution[.sync]`.
1245    /// Inject an activity task into the worker pool, skipping a
1246    /// state-machine execution. Used by tests that want to exercise the
1247    /// worker-pool API surface (`GetActivityTask` / `SendTaskSuccess`)
1248    /// without spinning up an ASL workflow.
1249    pub async fn enqueue_activity_task(
1250        &self,
1251        req: &SfnEnqueueActivityTaskRequest,
1252    ) -> Result<SfnEnqueueActivityTaskResponse, Error> {
1253        let resp = self
1254            .fc
1255            .client
1256            .post(format!(
1257                "{}/_fakecloud/stepfunctions/enqueue-activity-task",
1258                self.fc.base_url
1259            ))
1260            .json(req)
1261            .send()
1262            .await?;
1263        FakeCloud::parse(resp).await
1264    }
1265
1266    pub async fn get_execution_tree(
1267        &self,
1268        execution_arn: &str,
1269    ) -> Result<StepFunctionsExecutionTreeResponse, Error> {
1270        let mut encoded = String::with_capacity(execution_arn.len());
1271        for b in execution_arn.bytes() {
1272            match b {
1273                b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1274                    encoded.push(b as char);
1275                }
1276                _ => encoded.push_str(&format!("%{:02X}", b)),
1277            }
1278        }
1279        let resp = self
1280            .fc
1281            .client
1282            .get(format!(
1283                "{}/_fakecloud/stepfunctions/execution-tree/{}",
1284                self.fc.base_url, encoded
1285            ))
1286            .send()
1287            .await?;
1288        FakeCloud::parse(resp).await
1289    }
1290}
1291
1292// ── Bedrock ─────────────────────────────────────────────────────────
1293
1294pub struct BedrockClient<'a> {
1295    fc: &'a FakeCloud,
1296}
1297
1298impl BedrockClient<'_> {
1299    /// List recorded Bedrock runtime invocations. Each invocation has an optional
1300    /// `error` field that is set for calls faulted via [`Self::queue_fault`].
1301    pub async fn get_invocations(&self) -> Result<BedrockInvocationsResponse, Error> {
1302        let resp = self
1303            .fc
1304            .client
1305            .get(format!(
1306                "{}/_fakecloud/bedrock/invocations",
1307                self.fc.base_url
1308            ))
1309            .send()
1310            .await?;
1311        FakeCloud::parse(resp).await
1312    }
1313
1314    /// Configure a single canned response for a Bedrock model.
1315    pub async fn set_model_response(
1316        &self,
1317        model_id: &str,
1318        response: &str,
1319    ) -> Result<BedrockModelResponseConfig, Error> {
1320        let resp = self
1321            .fc
1322            .client
1323            .post(format!(
1324                "{}/_fakecloud/bedrock/models/{}/response",
1325                self.fc.base_url, model_id
1326            ))
1327            .header("content-type", "text/plain")
1328            .body(response.to_string())
1329            .send()
1330            .await?;
1331        FakeCloud::parse(resp).await
1332    }
1333
1334    /// Replace the prompt-conditional response rule list for a Bedrock model.
1335    pub async fn set_response_rules(
1336        &self,
1337        model_id: &str,
1338        rules: &[BedrockResponseRule],
1339    ) -> Result<BedrockModelResponseConfig, Error> {
1340        let body = serde_json::json!({ "rules": rules });
1341        let resp = self
1342            .fc
1343            .client
1344            .post(format!(
1345                "{}/_fakecloud/bedrock/models/{}/responses",
1346                self.fc.base_url, model_id
1347            ))
1348            .json(&body)
1349            .send()
1350            .await?;
1351        FakeCloud::parse(resp).await
1352    }
1353
1354    /// Clear all prompt-conditional response rules for a Bedrock model.
1355    pub async fn clear_response_rules(
1356        &self,
1357        model_id: &str,
1358    ) -> Result<BedrockModelResponseConfig, Error> {
1359        let resp = self
1360            .fc
1361            .client
1362            .delete(format!(
1363                "{}/_fakecloud/bedrock/models/{}/responses",
1364                self.fc.base_url, model_id
1365            ))
1366            .send()
1367            .await?;
1368        FakeCloud::parse(resp).await
1369    }
1370
1371    /// Queue a fault rule that will cause the next matching Bedrock runtime call(s) to fail.
1372    pub async fn queue_fault(
1373        &self,
1374        rule: &BedrockFaultRule,
1375    ) -> Result<BedrockStatusResponse, Error> {
1376        let resp = self
1377            .fc
1378            .client
1379            .post(format!("{}/_fakecloud/bedrock/faults", self.fc.base_url))
1380            .json(rule)
1381            .send()
1382            .await?;
1383        FakeCloud::parse(resp).await
1384    }
1385
1386    /// List currently queued fault rules.
1387    pub async fn get_faults(&self) -> Result<BedrockFaultsResponse, Error> {
1388        let resp = self
1389            .fc
1390            .client
1391            .get(format!("{}/_fakecloud/bedrock/faults", self.fc.base_url))
1392            .send()
1393            .await?;
1394        FakeCloud::parse(resp).await
1395    }
1396
1397    /// Clear all queued fault rules.
1398    pub async fn clear_faults(&self) -> Result<BedrockStatusResponse, Error> {
1399        let resp = self
1400            .fc
1401            .client
1402            .delete(format!("{}/_fakecloud/bedrock/faults", self.fc.base_url))
1403            .send()
1404            .await?;
1405        FakeCloud::parse(resp).await
1406    }
1407}
1408
1409// ── Bedrock Agent (control plane) ───────────────────────────────────
1410
1411pub struct BedrockAgentClient<'a> {
1412    fc: &'a FakeCloud,
1413}
1414
1415impl BedrockAgentClient<'_> {
1416    /// List every recorded Bedrock Agent with its aliases, versions,
1417    /// knowledge-base attachments, and collaborators flattened into one
1418    /// row each.
1419    pub async fn get_agents(&self) -> Result<BedrockAgentAgentsResponse, Error> {
1420        let resp = self
1421            .fc
1422            .client
1423            .get(format!(
1424                "{}/_fakecloud/bedrock-agent/agents",
1425                self.fc.base_url
1426            ))
1427            .send()
1428            .await?;
1429        FakeCloud::parse(resp).await
1430    }
1431}
1432
1433// ── Bedrock Agent Runtime (data plane) ──────────────────────────────
1434
1435pub struct BedrockAgentRuntimeClient<'a> {
1436    fc: &'a FakeCloud,
1437}
1438
1439impl BedrockAgentRuntimeClient<'_> {
1440    /// List every recorded InvokeAgent / InvokeInlineAgent / InvokeFlow
1441    /// / Retrieve / RetrieveAndGenerate / CreateInvocation call.
1442    pub async fn get_invocations(&self) -> Result<BedrockAgentRuntimeInvocationsResponse, Error> {
1443        let resp = self
1444            .fc
1445            .client
1446            .get(format!(
1447                "{}/_fakecloud/bedrock-agent-runtime/invocations",
1448                self.fc.base_url
1449            ))
1450            .send()
1451            .await?;
1452        FakeCloud::parse(resp).await
1453    }
1454}
1455
1456// ── ECS ─────────────────────────────────────────────────────────────
1457
1458pub struct EcsClient<'a> {
1459    fc: &'a FakeCloud,
1460}
1461
1462impl EcsClient<'_> {
1463    /// List all ECS clusters across every account the server has seen.
1464    /// Deterministic, sorted by cluster ARN. Bypasses the ECS control-plane
1465    /// auth and pagination so tests can assert directly on raw state.
1466    pub async fn get_clusters(&self) -> Result<EcsClustersResponse, Error> {
1467        let resp = self
1468            .fc
1469            .client
1470            .get(format!("{}/_fakecloud/ecs/clusters", self.fc.base_url))
1471            .send()
1472            .await?;
1473        FakeCloud::parse(resp).await
1474    }
1475
1476    /// List every task the server has seen. Optional `cluster` / `status`
1477    /// filters restrict the dump when supplied.
1478    pub async fn get_tasks(
1479        &self,
1480        cluster: Option<&str>,
1481        status: Option<&str>,
1482    ) -> Result<EcsTasksResponse, Error> {
1483        fn encode(s: &str) -> String {
1484            let mut out = String::with_capacity(s.len());
1485            for b in s.bytes() {
1486                match b {
1487                    b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1488                        out.push(b as char);
1489                    }
1490                    _ => out.push_str(&format!("%{:02X}", b)),
1491                }
1492            }
1493            out
1494        }
1495        let mut url = format!("{}/_fakecloud/ecs/tasks", self.fc.base_url);
1496        let mut sep = '?';
1497        if let Some(c) = cluster {
1498            url.push(sep);
1499            url.push_str("cluster=");
1500            url.push_str(&encode(c));
1501            sep = '&';
1502        }
1503        if let Some(s) = status {
1504            url.push(sep);
1505            url.push_str("status=");
1506            url.push_str(&encode(s));
1507        }
1508        let resp = self.fc.client.get(url).send().await?;
1509        FakeCloud::parse(resp).await
1510    }
1511
1512    /// Tail stored container stdout/stderr for a single task. Works even
1513    /// when no `awslogs` driver is configured — fakecloud always captures
1514    /// docker stdout/stderr on exit and keeps it on the task.
1515    pub async fn get_task_logs(&self, task_id: &str) -> Result<EcsTaskLogsResponse, Error> {
1516        let resp = self
1517            .fc
1518            .client
1519            .get(format!(
1520                "{}/_fakecloud/ecs/tasks/{}/logs",
1521                self.fc.base_url, task_id
1522            ))
1523            .send()
1524            .await?;
1525        FakeCloud::parse(resp).await
1526    }
1527
1528    /// Force the running container behind a task to stop.
1529    pub async fn force_stop_task(&self, task_id: &str) -> Result<EcsTask, Error> {
1530        let resp = self
1531            .fc
1532            .client
1533            .post(format!(
1534                "{}/_fakecloud/ecs/tasks/{}/force-stop",
1535                self.fc.base_url, task_id
1536            ))
1537            .send()
1538            .await?;
1539        FakeCloud::parse(resp).await
1540    }
1541
1542    /// Flip the task to STOPPED without killing the underlying container
1543    /// — useful for simulating task failures in tests.
1544    pub async fn mark_task_failed(
1545        &self,
1546        task_id: &str,
1547        req: &EcsMarkFailedRequest,
1548    ) -> Result<EcsTask, Error> {
1549        let resp = self
1550            .fc
1551            .client
1552            .post(format!(
1553                "{}/_fakecloud/ecs/tasks/{}/mark-failed",
1554                self.fc.base_url, task_id
1555            ))
1556            .json(req)
1557            .send()
1558            .await?;
1559        FakeCloud::parse(resp).await
1560    }
1561
1562    /// Replay the lifecycle event log.
1563    pub async fn get_events(&self) -> Result<EcsEventsResponse, Error> {
1564        let resp = self
1565            .fc
1566            .client
1567            .get(format!("{}/_fakecloud/ecs/events", self.fc.base_url))
1568            .send()
1569            .await?;
1570        FakeCloud::parse(resp).await
1571    }
1572
1573    /// Get the ECS task-metadata v4 dump keyed by full task ARN. Unlike
1574    /// the per-container `/_fakecloud/ecs/v4/{task_id}` endpoint, this
1575    /// is keyed by ARN for assertion-friendly use from tests that hold
1576    /// the `RunTask` response. Returned as raw JSON because the shape
1577    /// is the aggregated container-metadata document AWS surfaces at
1578    /// `ECS_CONTAINER_METADATA_URI_V4`.
1579    pub async fn get_metadata_by_arn(&self, task_arn: &str) -> Result<serde_json::Value, Error> {
1580        let mut encoded = String::with_capacity(task_arn.len());
1581        for b in task_arn.bytes() {
1582            match b {
1583                b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1584                    encoded.push(b as char);
1585                }
1586                _ => encoded.push_str(&format!("%{:02X}", b)),
1587            }
1588        }
1589        let resp = self
1590            .fc
1591            .client
1592            .get(format!(
1593                "{}/_fakecloud/ecs/metadata/{}",
1594                self.fc.base_url, encoded
1595            ))
1596            .send()
1597            .await?;
1598        FakeCloud::parse(resp).await
1599    }
1600
1601    /// Fetch the IAM task-role credentials that fakecloud hands out via
1602    /// `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` to the container. Fields
1603    /// use AWS's native PascalCase (`AccessKeyId`, `SecretAccessKey`,
1604    /// `Token`, `Expiration`, `RoleArn`).
1605    pub async fn task_credentials(
1606        &self,
1607        task_id: &str,
1608    ) -> Result<EcsTaskCredentialsResponse, Error> {
1609        let resp = self
1610            .fc
1611            .client
1612            .get(format!(
1613                "{}/_fakecloud/ecs/creds/{}",
1614                self.fc.base_url, task_id
1615            ))
1616            .send()
1617            .await?;
1618        FakeCloud::parse(resp).await
1619    }
1620
1621    /// Fetch the v3 ECS task metadata document for `task_id`. Returned
1622    /// as raw JSON (`Cluster`, `TaskARN`, `Family`, `Revision`,
1623    /// `DesiredStatus`, `KnownStatus`, `Containers`, `Limits`,
1624    /// `Networks`, ...) so callers can pick the fields they need
1625    /// without dragging in the entire metadata schema.
1626    pub async fn task_metadata_v3(&self, task_id: &str) -> Result<serde_json::Value, Error> {
1627        let resp = self
1628            .fc
1629            .client
1630            .get(format!(
1631                "{}/_fakecloud/ecs/v3/{}",
1632                self.fc.base_url, task_id
1633            ))
1634            .send()
1635            .await?;
1636        FakeCloud::parse(resp).await
1637    }
1638
1639    /// Fetch the v4 ECS task metadata document for `task_id`. Same
1640    /// pass-through shape as the v3 endpoint; v4 adds extra container
1641    /// runtime fields the server emits as-is.
1642    pub async fn task_metadata_v4(&self, task_id: &str) -> Result<serde_json::Value, Error> {
1643        let resp = self
1644            .fc
1645            .client
1646            .get(format!(
1647                "{}/_fakecloud/ecs/v4/{}",
1648                self.fc.base_url, task_id
1649            ))
1650            .send()
1651            .await?;
1652        FakeCloud::parse(resp).await
1653    }
1654}
1655
1656// ── Route 53 ────────────────────────────────────────────────────────
1657
1658pub struct Route53Client<'a> {
1659    fc: &'a FakeCloud,
1660}
1661
1662impl Route53Client<'_> {
1663    /// Fetch the deterministic DNSSEC chain-of-trust material for a
1664    /// hosted zone. Returns `Err(Error::Api { status: 404, .. })` when
1665    /// the zone has no ACTIVE KSK.
1666    pub async fn dnssec_material(
1667        &self,
1668        zone_id: &str,
1669    ) -> Result<Route53DnssecMaterialResponse, Error> {
1670        let resp = self
1671            .fc
1672            .client
1673            .get(format!(
1674                "{}/_fakecloud/route53/zones/{}/dnssec",
1675                self.fc.base_url, zone_id
1676            ))
1677            .send()
1678            .await?;
1679        FakeCloud::parse(resp).await
1680    }
1681
1682    /// Sign an RRset under the zone's first ACTIVE KSK and return the
1683    /// raw RRSIG fields so tests can verify the signature against the
1684    /// zone's DNSKEY material.
1685    pub async fn dnssec_sign(
1686        &self,
1687        zone_id: &str,
1688        req: &Route53DnssecSignRequest,
1689    ) -> Result<Route53DnssecSignResponse, Error> {
1690        let resp = self
1691            .fc
1692            .client
1693            .post(format!(
1694                "{}/_fakecloud/route53/zones/{}/dnssec/sign",
1695                self.fc.base_url, zone_id
1696            ))
1697            .json(req)
1698            .send()
1699            .await?;
1700        FakeCloud::parse(resp).await
1701    }
1702}
1703
1704// ── Athena ──────────────────────────────────────────────────────────
1705
1706pub struct AthenaClient<'a> {
1707    fc: &'a FakeCloud,
1708}
1709
1710impl AthenaClient<'_> {
1711    /// List every named query stored in the Athena named-query registry
1712    /// across all workgroups for the default account. Bumps `last_used_at`
1713    /// each time `StartQueryExecution` resolves a query by id so test
1714    /// authors can assert that a saved query was actually exercised.
1715    pub async fn get_named_queries(&self) -> Result<AthenaNamedQueriesResponse, Error> {
1716        let resp = self
1717            .fc
1718            .client
1719            .get(format!(
1720                "{}/_fakecloud/athena/named-queries",
1721                self.fc.base_url
1722            ))
1723            .send()
1724            .await?;
1725        FakeCloud::parse(resp).await
1726    }
1727}
1728
1729// ── Organizations ───────────────────────────────────────────────────
1730
1731pub struct OrganizationsClient<'a> {
1732    fc: &'a FakeCloud,
1733}
1734
1735impl OrganizationsClient<'_> {
1736    /// List every member account in the org with lifecycle state,
1737    /// parent OU, tags, and directly-attached SCPs. Returns an empty
1738    /// `accounts` list (and `None` for management/master ids) when no
1739    /// organization has been created yet.
1740    pub async fn get_accounts(&self) -> Result<OrganizationsAccountsResponse, Error> {
1741        let resp = self
1742            .fc
1743            .client
1744            .get(format!(
1745                "{}/_fakecloud/organizations/accounts",
1746                self.fc.base_url
1747            ))
1748            .send()
1749            .await?;
1750        FakeCloud::parse(resp).await
1751    }
1752
1753    /// List every billing-responsibility transfer in the org, with
1754    /// direction, lifecycle status, and the active handshake. Returns an
1755    /// empty list when no organization has been created.
1756    pub async fn get_responsibility_transfers(
1757        &self,
1758    ) -> Result<OrganizationsResponsibilityTransfersResponse, Error> {
1759        let resp = self
1760            .fc
1761            .client
1762            .get(format!(
1763                "{}/_fakecloud/organizations/responsibility-transfers",
1764                self.fc.base_url
1765            ))
1766            .send()
1767            .await?;
1768        FakeCloud::parse(resp).await
1769    }
1770}
1771
1772// ── ACM ─────────────────────────────────────────────────────────────
1773
1774pub struct AcmClient<'a> {
1775    fc: &'a FakeCloud,
1776}
1777
1778impl AcmClient<'_> {
1779    fn certificate_id(arn_or_id: &str) -> String {
1780        match arn_or_id.rfind("certificate/") {
1781            Some(idx) => arn_or_id[idx + "certificate/".len()..].to_string(),
1782            None => arn_or_id.to_string(),
1783        }
1784    }
1785
1786    /// Flip a stored ACM certificate's status (and optionally record a
1787    /// failure reason). Accepts either the full ACM ARN or just the
1788    /// trailing UUID. Returns `Error::Api { status: 404, .. }` if the
1789    /// certificate is unknown.
1790    pub async fn set_certificate_status(
1791        &self,
1792        arn_or_id: &str,
1793        req: &AcmCertificateStatusRequest,
1794    ) -> Result<(), Error> {
1795        let id = Self::certificate_id(arn_or_id);
1796        let resp = self
1797            .fc
1798            .client
1799            .post(format!(
1800                "{}/_fakecloud/acm/certificates/{}/status",
1801                self.fc.base_url, id
1802            ))
1803            .json(req)
1804            .send()
1805            .await?;
1806        if !resp.status().is_success() {
1807            let status = resp.status().as_u16();
1808            let body = resp.text().await.unwrap_or_default();
1809            return Err(Error::Api { status, body });
1810        }
1811        Ok(())
1812    }
1813
1814    /// Inspect a stored certificate's PEM block counts and byte sizes.
1815    /// `external_ca_validated` is always `false` — fakecloud does not run
1816    /// real X.509 verification.
1817    pub async fn get_certificate_chain_info(
1818        &self,
1819        arn_or_id: &str,
1820    ) -> Result<AcmCertificateChainInfo, Error> {
1821        let id = Self::certificate_id(arn_or_id);
1822        let resp = self
1823            .fc
1824            .client
1825            .get(format!(
1826                "{}/_fakecloud/acm/certificates/{}/chain-info",
1827                self.fc.base_url, id
1828            ))
1829            .send()
1830            .await?;
1831        FakeCloud::parse(resp).await
1832    }
1833
1834    /// Approve a `PENDING_VALIDATION` certificate (synchronous equivalent
1835    /// of "user clicked the validation link"). Flips the cert to `ISSUED`
1836    /// and refreshes its renewal eligibility / RenewalSummary.
1837    pub async fn approve_certificate(&self, arn_or_id: &str) -> Result<(), Error> {
1838        let id = Self::certificate_id(arn_or_id);
1839        let resp = self
1840            .fc
1841            .client
1842            .post(format!(
1843                "{}/_fakecloud/acm/certificates/{}/approve",
1844                self.fc.base_url, id
1845            ))
1846            .send()
1847            .await?;
1848        if !resp.status().is_success() {
1849            let status = resp.status().as_u16();
1850            let body = resp.text().await.unwrap_or_default();
1851            return Err(Error::Api { status, body });
1852        }
1853        Ok(())
1854    }
1855}
1856
1857// ── ECR ─────────────────────────────────────────────────────────────
1858
1859pub struct EcrClient<'a> {
1860    fc: &'a FakeCloud,
1861}
1862
1863impl EcrClient<'_> {
1864    /// List every ECR image across every repository.
1865    pub async fn get_images(&self) -> Result<EcrImagesResponse, Error> {
1866        let resp = self
1867            .fc
1868            .client
1869            .get(format!("{}/_fakecloud/ecr/images", self.fc.base_url))
1870            .send()
1871            .await?;
1872        FakeCloud::parse(resp).await
1873    }
1874
1875    /// List every ECR repository.
1876    pub async fn get_repositories(&self) -> Result<EcrRepositoriesResponse, Error> {
1877        let resp = self
1878            .fc
1879            .client
1880            .get(format!("{}/_fakecloud/ecr/repositories", self.fc.base_url))
1881            .send()
1882            .await?;
1883        FakeCloud::parse(resp).await
1884    }
1885
1886    /// List configured ECR pull-through-cache rules.
1887    pub async fn get_pull_through_rules(&self) -> Result<EcrPullThroughRulesResponse, Error> {
1888        let resp = self
1889            .fc
1890            .client
1891            .get(format!(
1892                "{}/_fakecloud/ecr/pull-through-rules",
1893                self.fc.base_url
1894            ))
1895            .send()
1896            .await?;
1897        FakeCloud::parse(resp).await
1898    }
1899}
1900
1901// ── ELBv2 ───────────────────────────────────────────────────────────
1902
1903pub struct Elbv2Client<'a> {
1904    fc: &'a FakeCloud,
1905}
1906
1907impl Elbv2Client<'_> {
1908    /// List every ELBv2 load balancer (ALB / NLB / GWLB).
1909    pub async fn get_load_balancers(&self) -> Result<Elbv2LoadBalancersResponse, Error> {
1910        let resp = self
1911            .fc
1912            .client
1913            .get(format!(
1914                "{}/_fakecloud/elbv2/load-balancers",
1915                self.fc.base_url
1916            ))
1917            .send()
1918            .await?;
1919        FakeCloud::parse(resp).await
1920    }
1921
1922    /// List every ELBv2 listener.
1923    pub async fn get_listeners(&self) -> Result<Elbv2ListenersResponse, Error> {
1924        let resp = self
1925            .fc
1926            .client
1927            .get(format!("{}/_fakecloud/elbv2/listeners", self.fc.base_url))
1928            .send()
1929            .await?;
1930        FakeCloud::parse(resp).await
1931    }
1932
1933    /// List every ELBv2 routing rule.
1934    pub async fn get_rules(&self) -> Result<Elbv2RulesResponse, Error> {
1935        let resp = self
1936            .fc
1937            .client
1938            .get(format!("{}/_fakecloud/elbv2/rules", self.fc.base_url))
1939            .send()
1940            .await?;
1941        FakeCloud::parse(resp).await
1942    }
1943
1944    /// List every ELBv2 target group with its registered targets and
1945    /// health-check configuration.
1946    pub async fn get_target_groups(&self) -> Result<Elbv2TargetGroupsResponse, Error> {
1947        let resp = self
1948            .fc
1949            .client
1950            .get(format!(
1951                "{}/_fakecloud/elbv2/target-groups",
1952                self.fc.base_url
1953            ))
1954            .send()
1955            .await?;
1956        FakeCloud::parse(resp).await
1957    }
1958
1959    /// Flush buffered access-log records to the configured S3 bucket
1960    /// now. Returns the number of records that were flushed.
1961    pub async fn flush_access_logs(&self) -> Result<Elbv2AccessLogsFlushResponse, Error> {
1962        let resp = self
1963            .fc
1964            .client
1965            .post(format!(
1966                "{}/_fakecloud/elbv2/access-logs/flush",
1967                self.fc.base_url
1968            ))
1969            .send()
1970            .await?;
1971        FakeCloud::parse(resp).await
1972    }
1973}
1974
1975// ── Glue ────────────────────────────────────────────────────────────
1976
1977pub struct GlueClient<'a> {
1978    fc: &'a FakeCloud,
1979}
1980
1981impl GlueClient<'_> {
1982    /// List every configured Glue job.
1983    pub async fn get_jobs(&self) -> Result<GlueJobsResponse, Error> {
1984        let resp = self
1985            .fc
1986            .client
1987            .get(format!("{}/_fakecloud/glue/jobs", self.fc.base_url))
1988            .send()
1989            .await?;
1990        FakeCloud::parse(resp).await
1991    }
1992
1993    /// List Glue JobRun records. Optionally scope to a single job by
1994    /// name (matches the `job_name` query parameter).
1995    pub async fn get_job_runs(&self, job_name: Option<&str>) -> Result<GlueJobRunsResponse, Error> {
1996        fn encode(s: &str) -> String {
1997            let mut out = String::with_capacity(s.len());
1998            for b in s.bytes() {
1999                match b {
2000                    b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
2001                        out.push(b as char);
2002                    }
2003                    _ => out.push_str(&format!("%{:02X}", b)),
2004                }
2005            }
2006            out
2007        }
2008        let mut url = format!("{}/_fakecloud/glue/job-runs", self.fc.base_url);
2009        if let Some(name) = job_name {
2010            url.push_str("?job_name=");
2011            url.push_str(&encode(name));
2012        }
2013        let resp = self.fc.client.get(url).send().await?;
2014        FakeCloud::parse(resp).await
2015    }
2016
2017    /// List every configured Glue crawler across all accounts.
2018    pub async fn get_crawlers(&self) -> Result<GlueCrawlersResponse, Error> {
2019        let resp = self
2020            .fc
2021            .client
2022            .get(format!("{}/_fakecloud/glue/crawlers", self.fc.base_url))
2023            .send()
2024            .await?;
2025        FakeCloud::parse(resp).await
2026    }
2027}
2028
2029// ── CloudWatch ──────────────────────────────────────────────────────
2030
2031pub struct CloudWatchClient<'a> {
2032    fc: &'a FakeCloud,
2033}
2034
2035impl CloudWatchClient<'_> {
2036    /// List every metric and composite alarm across all accounts and
2037    /// regions, flattened with current state.
2038    pub async fn get_alarms(&self) -> Result<CloudWatchAlarmsResponse, Error> {
2039        let resp = self
2040            .fc
2041            .client
2042            .get(format!("{}/_fakecloud/cloudwatch/alarms", self.fc.base_url))
2043            .send()
2044            .await?;
2045        FakeCloud::parse(resp).await
2046    }
2047
2048    /// List every unique metric series (account, region, namespace,
2049    /// metric, dimensions) with its datapoint count and latest value.
2050    pub async fn get_metrics(&self) -> Result<CloudWatchMetricsResponse, Error> {
2051        let resp = self
2052            .fc
2053            .client
2054            .get(format!(
2055                "{}/_fakecloud/cloudwatch/metrics",
2056                self.fc.base_url
2057            ))
2058            .send()
2059            .await?;
2060        FakeCloud::parse(resp).await
2061    }
2062}
2063
2064// ── Firehose ────────────────────────────────────────────────────────
2065
2066pub struct FirehoseClient<'a> {
2067    fc: &'a FakeCloud,
2068}
2069
2070impl FirehoseClient<'_> {
2071    /// List every Firehose delivery stream across all accounts and
2072    /// regions, with stream type, lifecycle status, encryption summary,
2073    /// and destination count.
2074    pub async fn get_delivery_streams(&self) -> Result<FirehoseDeliveryStreamsResponse, Error> {
2075        let resp = self
2076            .fc
2077            .client
2078            .get(format!(
2079                "{}/_fakecloud/firehose/delivery-streams",
2080                self.fc.base_url
2081            ))
2082            .send()
2083            .await?;
2084        FakeCloud::parse(resp).await
2085    }
2086}
2087
2088// ── Logs ────────────────────────────────────────────────────────────
2089
2090pub struct LogsClient<'a> {
2091    fc: &'a FakeCloud,
2092}
2093
2094impl LogsClient<'_> {
2095    /// Seed a synthetic CloudWatch Logs anomaly so tests can exercise
2096    /// `ListAnomalies` / `UpdateAnomaly` deterministically. Returns the
2097    /// minted anomaly id.
2098    pub async fn inject_anomaly(
2099        &self,
2100        req: &LogsAnomalyInjectRequest,
2101    ) -> Result<LogsAnomalyInjectResponse, Error> {
2102        let resp = self
2103            .fc
2104            .client
2105            .post(format!(
2106                "{}/_fakecloud/logs/anomalies/inject",
2107                self.fc.base_url
2108            ))
2109            .json(req)
2110            .send()
2111            .await?;
2112        FakeCloud::parse(resp).await
2113    }
2114
2115    /// Snapshot the per-delivery configuration (one row per
2116    /// `Delivery`), joined with the `log_type` of its associated
2117    /// `DeliverySource`.
2118    pub async fn get_delivery_config(&self) -> Result<LogsDeliveryConfigResponse, Error> {
2119        let resp = self
2120            .fc
2121            .client
2122            .get(format!(
2123                "{}/_fakecloud/logs/delivery-config",
2124                self.fc.base_url
2125            ))
2126            .send()
2127            .await?;
2128        FakeCloud::parse(resp).await
2129    }
2130
2131    /// Get the parsed `Fields` lists from a single log group's index
2132    /// policies. Returns `Error::Api { status: 404, .. }` if the log
2133    /// group is unknown.
2134    pub async fn get_field_indexes(
2135        &self,
2136        log_group_name: &str,
2137    ) -> Result<LogsFieldIndexesResponse, Error> {
2138        let resp = self
2139            .fc
2140            .client
2141            .get(format!(
2142                "{}/_fakecloud/logs/field-indexes/{}",
2143                self.fc.base_url,
2144                utf8_percent_encode(log_group_name, NON_ALPHANUMERIC)
2145            ))
2146            .send()
2147            .await?;
2148        FakeCloud::parse(resp).await
2149    }
2150}
2151
2152impl Route53Client<'_> {
2153    /// Flip a stored Route 53 health check's reported status (and
2154    /// optionally its last-failure observation) so tests can simulate
2155    /// failover scenarios without a live checker. Returns
2156    /// `Error::Api { status: 404, .. }` if the health check is unknown.
2157    pub async fn set_health_check_status(
2158        &self,
2159        id: &str,
2160        req: &Route53HealthCheckStatusRequest,
2161    ) -> Result<(), Error> {
2162        let resp = self
2163            .fc
2164            .client
2165            .post(format!(
2166                "{}/_fakecloud/route53/health-checks/{}/status",
2167                self.fc.base_url, id
2168            ))
2169            .json(req)
2170            .send()
2171            .await?;
2172        if !resp.status().is_success() {
2173            let status = resp.status().as_u16();
2174            let body = resp.text().await.unwrap_or_default();
2175            return Err(Error::Api { status, body });
2176        }
2177        Ok(())
2178    }
2179}
2180
2181// ── Scheduler ───────────────────────────────────────────────────────
2182
2183pub struct SchedulerClient<'a> {
2184    fc: &'a FakeCloud,
2185}
2186
2187impl SchedulerClient<'_> {
2188    /// List every EventBridge Scheduler schedule across every account
2189    /// and group.
2190    pub async fn get_schedules(&self) -> Result<SchedulerSchedulesResponse, Error> {
2191        let resp = self
2192            .fc
2193            .client
2194            .get(format!(
2195                "{}/_fakecloud/scheduler/schedules",
2196                self.fc.base_url
2197            ))
2198            .send()
2199            .await?;
2200        FakeCloud::parse(resp).await
2201    }
2202
2203    /// Fire a single schedule by `(group, name)` immediately, bypassing
2204    /// the cron tick. Returns the schedule + target ARN that received the
2205    /// invocation.
2206    pub async fn fire_schedule(
2207        &self,
2208        group: &str,
2209        name: &str,
2210    ) -> Result<FireScheduleResponse, Error> {
2211        let resp = self
2212            .fc
2213            .client
2214            .post(format!(
2215                "{}/_fakecloud/scheduler/fire/{}/{}",
2216                self.fc.base_url, group, name
2217            ))
2218            .send()
2219            .await?;
2220        FakeCloud::parse(resp).await
2221    }
2222}
2223
2224// ── SSM ─────────────────────────────────────────────────────────────
2225
2226pub struct SsmClient<'a> {
2227    fc: &'a FakeCloud,
2228}
2229
2230impl SsmClient<'_> {
2231    /// Force a specific SSM command's status. Useful for driving the
2232    /// `Pending`/`InProgress`/`Success` lifecycle synchronously in tests.
2233    pub async fn set_command_status(
2234        &self,
2235        command_id: &str,
2236        req: &SetSsmCommandStatusRequest,
2237    ) -> Result<SetSsmCommandStatusResponse, Error> {
2238        let resp = self
2239            .fc
2240            .client
2241            .post(format!(
2242                "{}/_fakecloud/ssm/commands/{}/status",
2243                self.fc.base_url, command_id
2244            ))
2245            .json(req)
2246            .send()
2247            .await?;
2248        FakeCloud::parse(resp).await
2249    }
2250
2251    /// Flip a command's invocations to `Failed`. `req` is optional; when
2252    /// `None`, the server uses default values (all invocations, default
2253    /// account, generic "Failed" status detail).
2254    pub async fn fail_command(
2255        &self,
2256        command_id: &str,
2257        req: Option<&FailSsmCommandRequest>,
2258    ) -> Result<FailSsmCommandResponse, Error> {
2259        let mut builder = self.fc.client.post(format!(
2260            "{}/_fakecloud/ssm/commands/{}/fail",
2261            self.fc.base_url, command_id
2262        ));
2263        if let Some(body) = req {
2264            builder = builder.json(body);
2265        }
2266        let resp = builder.send().await?;
2267        FakeCloud::parse(resp).await
2268    }
2269
2270    /// List recorded parameter-policy events for an account. Pass `None`
2271    /// to use the server's default account.
2272    pub async fn parameter_policy_events(
2273        &self,
2274        account_id: Option<&str>,
2275    ) -> Result<SsmParameterPolicyEventsResponse, Error> {
2276        let mut url = format!(
2277            "{}/_fakecloud/ssm/parameter-policy-events",
2278            self.fc.base_url
2279        );
2280        if let Some(id) = account_id {
2281            url.push_str("?accountId=");
2282            url.push_str(id);
2283        }
2284        let resp = self.fc.client.get(url).send().await?;
2285        FakeCloud::parse(resp).await
2286    }
2287
2288    /// Inject a fake SSM session record so tests can exercise
2289    /// `DescribeSessions`/`TerminateSession` without going through
2290    /// `StartSession`.
2291    pub async fn inject_session(
2292        &self,
2293        req: &InjectSsmSessionRequest,
2294    ) -> Result<InjectSsmSessionResponse, Error> {
2295        let resp = self
2296            .fc
2297            .client
2298            .post(format!(
2299                "{}/_fakecloud/ssm/sessions/inject",
2300                self.fc.base_url
2301            ))
2302            .json(req)
2303            .send()
2304            .await?;
2305        FakeCloud::parse(resp).await
2306    }
2307}
2308
2309// ── KMS ─────────────────────────────────────────────────────────────
2310
2311pub struct KmsClient<'a> {
2312    fc: &'a FakeCloud,
2313}
2314
2315impl KmsClient<'_> {
2316    /// List recorded KMS data-plane invocations.
2317    pub async fn usage(&self) -> Result<KmsUsageResponse, Error> {
2318        let resp = self
2319            .fc
2320            .client
2321            .get(format!("{}/_fakecloud/kms/usage", self.fc.base_url))
2322            .send()
2323            .await?;
2324        FakeCloud::parse(resp).await
2325    }
2326}
2327
2328// ── WAFv2 ───────────────────────────────────────────────────────────
2329
2330pub struct WafV2Client<'a> {
2331    fc: &'a FakeCloud,
2332}
2333
2334impl WafV2Client<'_> {
2335    /// Evaluate a synthetic request against a `WebACL` and return the
2336    /// raw verdict JSON. Both request and response shapes are
2337    /// intentionally free-form so the SDK doesn't have to track every
2338    /// new field the evaluator emits.
2339    pub async fn evaluate(&self, body: &serde_json::Value) -> Result<serde_json::Value, Error> {
2340        let resp = self
2341            .fc
2342            .client
2343            .post(format!("{}/_fakecloud/wafv2/evaluate", self.fc.base_url))
2344            .json(body)
2345            .send()
2346            .await?;
2347        FakeCloud::parse(resp).await
2348    }
2349}
2350
2351// ── CloudFront ──────────────────────────────────────────────────────
2352
2353pub struct CloudFrontClient<'a> {
2354    fc: &'a FakeCloud,
2355}
2356
2357impl CloudFrontClient<'_> {
2358    /// Force a stored CloudFront Distribution into a new status (typically
2359    /// `"Deployed"` or `"InProgress"`). Returns an `Api { status: 404, .. }`
2360    /// error when the distribution doesn't exist.
2361    pub async fn set_distribution_status(
2362        &self,
2363        distribution_id: &str,
2364        req: &CloudFrontDistributionStatusRequest,
2365    ) -> Result<(), Error> {
2366        let resp = self
2367            .fc
2368            .client
2369            .post(format!(
2370                "{}/_fakecloud/cloudfront/distributions/{}/status",
2371                self.fc.base_url, distribution_id
2372            ))
2373            .json(req)
2374            .send()
2375            .await?;
2376        let status = resp.status().as_u16();
2377        if !resp.status().is_success() {
2378            let body = resp.text().await.unwrap_or_default();
2379            return Err(Error::Api { status, body });
2380        }
2381        Ok(())
2382    }
2383}
2384
2385impl Elbv2Client<'_> {
2386    /// Snapshot the ELBv2 WAF count-metric registry. The `counts` field
2387    /// is intentionally free-form JSON because its shape tracks the
2388    /// service-internal metric layout.
2389    pub async fn waf_counts(&self) -> Result<Elbv2WafCountsResponse, Error> {
2390        let resp = self
2391            .fc
2392            .client
2393            .get(format!("{}/_fakecloud/elbv2/waf-counts", self.fc.base_url))
2394            .send()
2395            .await?;
2396        FakeCloud::parse(resp).await
2397    }
2398}