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