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