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