Skip to main content

fakecloud_sdk/
client.rs

1use crate::error::Error;
2use crate::types::*;
3
4/// Client for the fakecloud introspection and simulation API (`/_fakecloud/*`).
5pub struct FakeCloud {
6    base_url: String,
7    client: reqwest::Client,
8}
9
10impl FakeCloud {
11    /// Create a new client pointing at the given fakecloud base URL (e.g. `http://localhost:4566`).
12    pub fn new(base_url: &str) -> Self {
13        Self {
14            base_url: base_url.trim_end_matches('/').to_string(),
15            client: reqwest::Client::new(),
16        }
17    }
18
19    // ── Health & Reset ──────────────────────────────────────────────
20
21    /// Check server health.
22    pub async fn health(&self) -> Result<HealthResponse, Error> {
23        let resp = self
24            .client
25            .get(format!("{}/_fakecloud/health", self.base_url))
26            .send()
27            .await?;
28        Self::parse(resp).await
29    }
30
31    /// Reset all service state. Uses the legacy `/_reset` endpoint.
32    pub async fn reset(&self) -> Result<ResetResponse, Error> {
33        let resp = self
34            .client
35            .post(format!("{}/_reset", self.base_url))
36            .send()
37            .await?;
38        Self::parse(resp).await
39    }
40
41    /// Create an IAM admin user in a specific account. Returns credentials
42    /// for the new user. Solves the multi-account bootstrap problem: the
43    /// root bypass only targets the default account, so this endpoint lets
44    /// callers create credentials for any account.
45    pub async fn create_admin(
46        &self,
47        account_id: &str,
48        user_name: &str,
49    ) -> Result<CreateAdminResponse, Error> {
50        let resp = self
51            .client
52            .post(format!("{}/_fakecloud/iam/create-admin", self.base_url))
53            .json(&CreateAdminRequest {
54                account_id: account_id.to_string(),
55                user_name: user_name.to_string(),
56            })
57            .send()
58            .await?;
59        Self::parse(resp).await
60    }
61
62    /// Reset a single service's state.
63    pub async fn reset_service(&self, service: &str) -> Result<ResetServiceResponse, Error> {
64        let resp = self
65            .client
66            .post(format!("{}/_fakecloud/reset/{}", self.base_url, service))
67            .send()
68            .await?;
69        Self::parse(resp).await
70    }
71
72    /// Reset a single service's state for a specific account only.
73    pub async fn reset_service_for_account(
74        &self,
75        service: &str,
76        account_id: &str,
77    ) -> Result<ResetServiceResponse, Error> {
78        let resp = self
79            .client
80            .post(format!(
81                "{}/_fakecloud/reset/{}/{}",
82                self.base_url, service, account_id
83            ))
84            .send()
85            .await?;
86        Self::parse(resp).await
87    }
88
89    // ── Sub-clients ─────────────────────────────────────────────────
90
91    pub fn lambda(&self) -> LambdaClient<'_> {
92        LambdaClient { fc: self }
93    }
94
95    pub fn ses(&self) -> SesClient<'_> {
96        SesClient { fc: self }
97    }
98
99    pub fn sns(&self) -> SnsClient<'_> {
100        SnsClient { fc: self }
101    }
102
103    pub fn sqs(&self) -> SqsClient<'_> {
104        SqsClient { fc: self }
105    }
106
107    pub fn events(&self) -> EventsClient<'_> {
108        EventsClient { fc: self }
109    }
110
111    pub fn s3(&self) -> S3Client<'_> {
112        S3Client { fc: self }
113    }
114
115    pub fn dynamodb(&self) -> DynamoDbClient<'_> {
116        DynamoDbClient { fc: self }
117    }
118
119    pub fn secretsmanager(&self) -> SecretsManagerClient<'_> {
120        SecretsManagerClient { fc: self }
121    }
122
123    pub fn cognito(&self) -> CognitoClient<'_> {
124        CognitoClient { fc: self }
125    }
126
127    pub fn rds(&self) -> RdsClient<'_> {
128        RdsClient { fc: self }
129    }
130
131    pub fn elasticache(&self) -> ElastiCacheClient<'_> {
132        ElastiCacheClient { fc: self }
133    }
134
135    pub fn apigatewayv2(&self) -> ApiGatewayV2Client<'_> {
136        ApiGatewayV2Client { fc: self }
137    }
138
139    pub fn stepfunctions(&self) -> StepFunctionsClient<'_> {
140        StepFunctionsClient { fc: self }
141    }
142
143    pub fn bedrock(&self) -> BedrockClient<'_> {
144        BedrockClient { fc: self }
145    }
146
147    pub fn ecs(&self) -> EcsClient<'_> {
148        EcsClient { fc: self }
149    }
150
151    // ── Internal helpers ────────────────────────────────────────────
152
153    async fn parse<T: serde::de::DeserializeOwned>(resp: reqwest::Response) -> Result<T, Error> {
154        let status = resp.status().as_u16();
155        if !resp.status().is_success() {
156            let body = resp.text().await.unwrap_or_default();
157            return Err(Error::Api { status, body });
158        }
159        Ok(resp.json::<T>().await?)
160    }
161}
162
163// ── RDS ─────────────────────────────────────────────────────────────
164
165pub struct RdsClient<'a> {
166    fc: &'a FakeCloud,
167}
168
169impl RdsClient<'_> {
170    /// List fakecloud-managed RDS DB instances with runtime metadata.
171    pub async fn get_instances(&self) -> Result<RdsInstancesResponse, Error> {
172        let resp = self
173            .fc
174            .client
175            .get(format!("{}/_fakecloud/rds/instances", self.fc.base_url))
176            .send()
177            .await?;
178        FakeCloud::parse(resp).await
179    }
180}
181
182// ── ElastiCache ─────────────────────────────────────────────────────
183
184pub struct ElastiCacheClient<'a> {
185    fc: &'a FakeCloud,
186}
187
188impl ElastiCacheClient<'_> {
189    /// List fakecloud-managed ElastiCache cache clusters with runtime metadata.
190    pub async fn get_clusters(&self) -> Result<ElastiCacheClustersResponse, Error> {
191        let resp = self
192            .fc
193            .client
194            .get(format!(
195                "{}/_fakecloud/elasticache/clusters",
196                self.fc.base_url
197            ))
198            .send()
199            .await?;
200        FakeCloud::parse(resp).await
201    }
202
203    /// List fakecloud-managed ElastiCache replication groups with runtime metadata.
204    pub async fn get_replication_groups(
205        &self,
206    ) -> Result<ElastiCacheReplicationGroupsResponse, Error> {
207        let resp = self
208            .fc
209            .client
210            .get(format!(
211                "{}/_fakecloud/elasticache/replication-groups",
212                self.fc.base_url
213            ))
214            .send()
215            .await?;
216        FakeCloud::parse(resp).await
217    }
218
219    /// List fakecloud-managed ElastiCache serverless caches with runtime metadata.
220    pub async fn get_serverless_caches(
221        &self,
222    ) -> Result<ElastiCacheServerlessCachesResponse, Error> {
223        let resp = self
224            .fc
225            .client
226            .get(format!(
227                "{}/_fakecloud/elasticache/serverless-caches",
228                self.fc.base_url
229            ))
230            .send()
231            .await?;
232        FakeCloud::parse(resp).await
233    }
234}
235
236// ── Lambda ──────────────────────────────────────────────────────────
237
238pub struct LambdaClient<'a> {
239    fc: &'a FakeCloud,
240}
241
242impl LambdaClient<'_> {
243    /// List recorded Lambda invocations.
244    pub async fn get_invocations(&self) -> Result<LambdaInvocationsResponse, Error> {
245        let resp = self
246            .fc
247            .client
248            .get(format!(
249                "{}/_fakecloud/lambda/invocations",
250                self.fc.base_url
251            ))
252            .send()
253            .await?;
254        FakeCloud::parse(resp).await
255    }
256
257    /// List warm (cached) Lambda containers.
258    pub async fn get_warm_containers(&self) -> Result<WarmContainersResponse, Error> {
259        let resp = self
260            .fc
261            .client
262            .get(format!(
263                "{}/_fakecloud/lambda/warm-containers",
264                self.fc.base_url
265            ))
266            .send()
267            .await?;
268        FakeCloud::parse(resp).await
269    }
270
271    /// Evict the warm container for a specific function.
272    pub async fn evict_container(
273        &self,
274        function_name: &str,
275    ) -> Result<EvictContainerResponse, Error> {
276        let resp = self
277            .fc
278            .client
279            .post(format!(
280                "{}/_fakecloud/lambda/{}/evict-container",
281                self.fc.base_url, function_name
282            ))
283            .send()
284            .await?;
285        FakeCloud::parse(resp).await
286    }
287}
288
289// ── SES ─────────────────────────────────────────────────────────────
290
291pub struct SesClient<'a> {
292    fc: &'a FakeCloud,
293}
294
295impl SesClient<'_> {
296    /// List all sent emails.
297    pub async fn get_emails(&self) -> Result<SesEmailsResponse, Error> {
298        let resp = self
299            .fc
300            .client
301            .get(format!("{}/_fakecloud/ses/emails", self.fc.base_url))
302            .send()
303            .await?;
304        FakeCloud::parse(resp).await
305    }
306
307    /// Simulate an inbound email (SES receipt rules).
308    pub async fn simulate_inbound(
309        &self,
310        req: &InboundEmailRequest,
311    ) -> Result<InboundEmailResponse, Error> {
312        let resp = self
313            .fc
314            .client
315            .post(format!("{}/_fakecloud/ses/inbound", self.fc.base_url))
316            .json(req)
317            .send()
318            .await?;
319        FakeCloud::parse(resp).await
320    }
321}
322
323// ── SNS ─────────────────────────────────────────────────────────────
324
325pub struct SnsClient<'a> {
326    fc: &'a FakeCloud,
327}
328
329impl SnsClient<'_> {
330    /// List all published SNS messages.
331    pub async fn get_messages(&self) -> Result<SnsMessagesResponse, Error> {
332        let resp = self
333            .fc
334            .client
335            .get(format!("{}/_fakecloud/sns/messages", self.fc.base_url))
336            .send()
337            .await?;
338        FakeCloud::parse(resp).await
339    }
340
341    /// List subscriptions pending confirmation.
342    pub async fn get_pending_confirmations(&self) -> Result<PendingConfirmationsResponse, Error> {
343        let resp = self
344            .fc
345            .client
346            .get(format!(
347                "{}/_fakecloud/sns/pending-confirmations",
348                self.fc.base_url
349            ))
350            .send()
351            .await?;
352        FakeCloud::parse(resp).await
353    }
354
355    /// Confirm a pending subscription.
356    pub async fn confirm_subscription(
357        &self,
358        req: &ConfirmSubscriptionRequest,
359    ) -> Result<ConfirmSubscriptionResponse, Error> {
360        let resp = self
361            .fc
362            .client
363            .post(format!(
364                "{}/_fakecloud/sns/confirm-subscription",
365                self.fc.base_url
366            ))
367            .json(req)
368            .send()
369            .await?;
370        FakeCloud::parse(resp).await
371    }
372}
373
374// ── SQS ─────────────────────────────────────────────────────────────
375
376pub struct SqsClient<'a> {
377    fc: &'a FakeCloud,
378}
379
380impl SqsClient<'_> {
381    /// List all messages across all queues.
382    pub async fn get_messages(&self) -> Result<SqsMessagesResponse, Error> {
383        let resp = self
384            .fc
385            .client
386            .get(format!("{}/_fakecloud/sqs/messages", self.fc.base_url))
387            .send()
388            .await?;
389        FakeCloud::parse(resp).await
390    }
391
392    /// Tick the message expiration processor (expire visibility-timed-out messages).
393    pub async fn tick_expiration(&self) -> Result<ExpirationTickResponse, Error> {
394        let resp = self
395            .fc
396            .client
397            .post(format!(
398                "{}/_fakecloud/sqs/expiration-processor/tick",
399                self.fc.base_url
400            ))
401            .send()
402            .await?;
403        FakeCloud::parse(resp).await
404    }
405
406    /// Force all messages in a queue to its DLQ.
407    pub async fn force_dlq(&self, queue_name: &str) -> Result<ForceDlqResponse, Error> {
408        let resp = self
409            .fc
410            .client
411            .post(format!(
412                "{}/_fakecloud/sqs/{}/force-dlq",
413                self.fc.base_url, queue_name
414            ))
415            .send()
416            .await?;
417        FakeCloud::parse(resp).await
418    }
419}
420
421// ── EventBridge ─────────────────────────────────────────────────────
422
423pub struct EventsClient<'a> {
424    fc: &'a FakeCloud,
425}
426
427impl EventsClient<'_> {
428    /// Get event history and delivery records.
429    pub async fn get_history(&self) -> Result<EventHistoryResponse, Error> {
430        let resp = self
431            .fc
432            .client
433            .get(format!("{}/_fakecloud/events/history", self.fc.base_url))
434            .send()
435            .await?;
436        FakeCloud::parse(resp).await
437    }
438
439    /// Fire a specific EventBridge rule manually.
440    pub async fn fire_rule(&self, req: &FireRuleRequest) -> Result<FireRuleResponse, Error> {
441        let resp = self
442            .fc
443            .client
444            .post(format!("{}/_fakecloud/events/fire-rule", self.fc.base_url))
445            .json(req)
446            .send()
447            .await?;
448        FakeCloud::parse(resp).await
449    }
450}
451
452// ── S3 ──────────────────────────────────────────────────────────────
453
454pub struct S3Client<'a> {
455    fc: &'a FakeCloud,
456}
457
458impl S3Client<'_> {
459    /// List S3 notification events.
460    pub async fn get_notifications(&self) -> Result<S3NotificationsResponse, Error> {
461        let resp = self
462            .fc
463            .client
464            .get(format!("{}/_fakecloud/s3/notifications", self.fc.base_url))
465            .send()
466            .await?;
467        FakeCloud::parse(resp).await
468    }
469
470    /// Tick the S3 lifecycle processor.
471    pub async fn tick_lifecycle(&self) -> Result<LifecycleTickResponse, Error> {
472        let resp = self
473            .fc
474            .client
475            .post(format!(
476                "{}/_fakecloud/s3/lifecycle-processor/tick",
477                self.fc.base_url
478            ))
479            .send()
480            .await?;
481        FakeCloud::parse(resp).await
482    }
483}
484
485// ── DynamoDB ────────────────────────────────────────────────────────
486
487pub struct DynamoDbClient<'a> {
488    fc: &'a FakeCloud,
489}
490
491impl DynamoDbClient<'_> {
492    /// Tick the DynamoDB TTL processor.
493    pub async fn tick_ttl(&self) -> Result<TtlTickResponse, Error> {
494        let resp = self
495            .fc
496            .client
497            .post(format!(
498                "{}/_fakecloud/dynamodb/ttl-processor/tick",
499                self.fc.base_url
500            ))
501            .send()
502            .await?;
503        FakeCloud::parse(resp).await
504    }
505}
506
507// ── SecretsManager ──────────────────────────────────────────────────
508
509pub struct SecretsManagerClient<'a> {
510    fc: &'a FakeCloud,
511}
512
513impl SecretsManagerClient<'_> {
514    /// Tick the SecretsManager rotation scheduler.
515    pub async fn tick_rotation(&self) -> Result<RotationTickResponse, Error> {
516        let resp = self
517            .fc
518            .client
519            .post(format!(
520                "{}/_fakecloud/secretsmanager/rotation-scheduler/tick",
521                self.fc.base_url
522            ))
523            .send()
524            .await?;
525        FakeCloud::parse(resp).await
526    }
527}
528
529// ── Cognito ─────────────────────────────────────────────────────────
530
531pub struct CognitoClient<'a> {
532    fc: &'a FakeCloud,
533}
534
535impl CognitoClient<'_> {
536    /// Get confirmation codes for a specific user.
537    pub async fn get_user_codes(
538        &self,
539        pool_id: &str,
540        username: &str,
541    ) -> Result<UserConfirmationCodes, Error> {
542        let resp = self
543            .fc
544            .client
545            .get(format!(
546                "{}/_fakecloud/cognito/confirmation-codes/{}/{}",
547                self.fc.base_url, pool_id, username
548            ))
549            .send()
550            .await?;
551        FakeCloud::parse(resp).await
552    }
553
554    /// List all confirmation codes across all pools.
555    pub async fn get_confirmation_codes(&self) -> Result<ConfirmationCodesResponse, Error> {
556        let resp = self
557            .fc
558            .client
559            .get(format!(
560                "{}/_fakecloud/cognito/confirmation-codes",
561                self.fc.base_url
562            ))
563            .send()
564            .await?;
565        FakeCloud::parse(resp).await
566    }
567
568    /// Confirm a user (bypass email/phone verification).
569    pub async fn confirm_user(
570        &self,
571        req: &ConfirmUserRequest,
572    ) -> Result<ConfirmUserResponse, Error> {
573        let resp = self
574            .fc
575            .client
576            .post(format!(
577                "{}/_fakecloud/cognito/confirm-user",
578                self.fc.base_url
579            ))
580            .json(req)
581            .send()
582            .await?;
583        let status = resp.status().as_u16();
584        let body: ConfirmUserResponse = resp.json().await?;
585        if status >= 400 {
586            return Err(Error::Api {
587                status,
588                body: body.error.unwrap_or_default(),
589            });
590        }
591        Ok(body)
592    }
593
594    /// List all active tokens.
595    pub async fn get_tokens(&self) -> Result<TokensResponse, Error> {
596        let resp = self
597            .fc
598            .client
599            .get(format!("{}/_fakecloud/cognito/tokens", self.fc.base_url))
600            .send()
601            .await?;
602        FakeCloud::parse(resp).await
603    }
604
605    /// Expire tokens (optionally filtered by pool/user).
606    pub async fn expire_tokens(
607        &self,
608        req: &ExpireTokensRequest,
609    ) -> Result<ExpireTokensResponse, Error> {
610        let resp = self
611            .fc
612            .client
613            .post(format!(
614                "{}/_fakecloud/cognito/expire-tokens",
615                self.fc.base_url
616            ))
617            .json(req)
618            .send()
619            .await?;
620        FakeCloud::parse(resp).await
621    }
622
623    /// List auth events.
624    pub async fn get_auth_events(&self) -> Result<AuthEventsResponse, Error> {
625        let resp = self
626            .fc
627            .client
628            .get(format!(
629                "{}/_fakecloud/cognito/auth-events",
630                self.fc.base_url
631            ))
632            .send()
633            .await?;
634        FakeCloud::parse(resp).await
635    }
636}
637
638// ── API Gateway v2 ──────────────────────────────────────────────────
639
640pub struct ApiGatewayV2Client<'a> {
641    fc: &'a FakeCloud,
642}
643
644impl ApiGatewayV2Client<'_> {
645    /// List all HTTP API requests that were received and processed.
646    pub async fn get_requests(&self) -> Result<ApiGatewayV2RequestsResponse, Error> {
647        let resp = self
648            .fc
649            .client
650            .get(format!(
651                "{}/_fakecloud/apigatewayv2/requests",
652                self.fc.base_url
653            ))
654            .send()
655            .await?;
656        FakeCloud::parse(resp).await
657    }
658}
659
660// ── Step Functions ──────────────────────────────────────────────────
661
662pub struct StepFunctionsClient<'a> {
663    fc: &'a FakeCloud,
664}
665
666impl StepFunctionsClient<'_> {
667    /// List all Step Functions executions with status, input, output, and timestamps.
668    pub async fn get_executions(&self) -> Result<StepFunctionsExecutionsResponse, Error> {
669        let resp = self
670            .fc
671            .client
672            .get(format!(
673                "{}/_fakecloud/stepfunctions/executions",
674                self.fc.base_url
675            ))
676            .send()
677            .await?;
678        FakeCloud::parse(resp).await
679    }
680}
681
682// ── Bedrock ─────────────────────────────────────────────────────────
683
684pub struct BedrockClient<'a> {
685    fc: &'a FakeCloud,
686}
687
688impl BedrockClient<'_> {
689    /// List recorded Bedrock runtime invocations. Each invocation has an optional
690    /// `error` field that is set for calls faulted via [`Self::queue_fault`].
691    pub async fn get_invocations(&self) -> Result<BedrockInvocationsResponse, Error> {
692        let resp = self
693            .fc
694            .client
695            .get(format!(
696                "{}/_fakecloud/bedrock/invocations",
697                self.fc.base_url
698            ))
699            .send()
700            .await?;
701        FakeCloud::parse(resp).await
702    }
703
704    /// Configure a single canned response for a Bedrock model.
705    pub async fn set_model_response(
706        &self,
707        model_id: &str,
708        response: &str,
709    ) -> Result<BedrockModelResponseConfig, Error> {
710        let resp = self
711            .fc
712            .client
713            .post(format!(
714                "{}/_fakecloud/bedrock/models/{}/response",
715                self.fc.base_url, model_id
716            ))
717            .header("content-type", "text/plain")
718            .body(response.to_string())
719            .send()
720            .await?;
721        FakeCloud::parse(resp).await
722    }
723
724    /// Replace the prompt-conditional response rule list for a Bedrock model.
725    pub async fn set_response_rules(
726        &self,
727        model_id: &str,
728        rules: &[BedrockResponseRule],
729    ) -> Result<BedrockModelResponseConfig, Error> {
730        let body = serde_json::json!({ "rules": rules });
731        let resp = self
732            .fc
733            .client
734            .post(format!(
735                "{}/_fakecloud/bedrock/models/{}/responses",
736                self.fc.base_url, model_id
737            ))
738            .json(&body)
739            .send()
740            .await?;
741        FakeCloud::parse(resp).await
742    }
743
744    /// Clear all prompt-conditional response rules for a Bedrock model.
745    pub async fn clear_response_rules(
746        &self,
747        model_id: &str,
748    ) -> Result<BedrockModelResponseConfig, Error> {
749        let resp = self
750            .fc
751            .client
752            .delete(format!(
753                "{}/_fakecloud/bedrock/models/{}/responses",
754                self.fc.base_url, model_id
755            ))
756            .send()
757            .await?;
758        FakeCloud::parse(resp).await
759    }
760
761    /// Queue a fault rule that will cause the next matching Bedrock runtime call(s) to fail.
762    pub async fn queue_fault(
763        &self,
764        rule: &BedrockFaultRule,
765    ) -> Result<BedrockStatusResponse, Error> {
766        let resp = self
767            .fc
768            .client
769            .post(format!("{}/_fakecloud/bedrock/faults", self.fc.base_url))
770            .json(rule)
771            .send()
772            .await?;
773        FakeCloud::parse(resp).await
774    }
775
776    /// List currently queued fault rules.
777    pub async fn get_faults(&self) -> Result<BedrockFaultsResponse, Error> {
778        let resp = self
779            .fc
780            .client
781            .get(format!("{}/_fakecloud/bedrock/faults", self.fc.base_url))
782            .send()
783            .await?;
784        FakeCloud::parse(resp).await
785    }
786
787    /// Clear all queued fault rules.
788    pub async fn clear_faults(&self) -> Result<BedrockStatusResponse, Error> {
789        let resp = self
790            .fc
791            .client
792            .delete(format!("{}/_fakecloud/bedrock/faults", self.fc.base_url))
793            .send()
794            .await?;
795        FakeCloud::parse(resp).await
796    }
797}
798
799// ── ECS ─────────────────────────────────────────────────────────────
800
801pub struct EcsClient<'a> {
802    fc: &'a FakeCloud,
803}
804
805impl EcsClient<'_> {
806    /// List all ECS clusters across every account the server has seen.
807    /// Deterministic, sorted by cluster ARN. Bypasses the ECS control-plane
808    /// auth and pagination so tests can assert directly on raw state.
809    pub async fn get_clusters(&self) -> Result<EcsClustersResponse, Error> {
810        let resp = self
811            .fc
812            .client
813            .get(format!("{}/_fakecloud/ecs/clusters", self.fc.base_url))
814            .send()
815            .await?;
816        FakeCloud::parse(resp).await
817    }
818
819    /// List every task the server has seen. Optional `cluster` / `status`
820    /// filters restrict the dump when supplied.
821    pub async fn get_tasks(
822        &self,
823        cluster: Option<&str>,
824        status: Option<&str>,
825    ) -> Result<EcsTasksResponse, Error> {
826        fn encode(s: &str) -> String {
827            let mut out = String::with_capacity(s.len());
828            for b in s.bytes() {
829                match b {
830                    b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
831                        out.push(b as char);
832                    }
833                    _ => out.push_str(&format!("%{:02X}", b)),
834                }
835            }
836            out
837        }
838        let mut url = format!("{}/_fakecloud/ecs/tasks", self.fc.base_url);
839        let mut sep = '?';
840        if let Some(c) = cluster {
841            url.push(sep);
842            url.push_str("cluster=");
843            url.push_str(&encode(c));
844            sep = '&';
845        }
846        if let Some(s) = status {
847            url.push(sep);
848            url.push_str("status=");
849            url.push_str(&encode(s));
850        }
851        let resp = self.fc.client.get(url).send().await?;
852        FakeCloud::parse(resp).await
853    }
854
855    /// Tail stored container stdout/stderr for a single task. Works even
856    /// when no `awslogs` driver is configured — fakecloud always captures
857    /// docker stdout/stderr on exit and keeps it on the task.
858    pub async fn get_task_logs(&self, task_id: &str) -> Result<EcsTaskLogsResponse, Error> {
859        let resp = self
860            .fc
861            .client
862            .get(format!(
863                "{}/_fakecloud/ecs/tasks/{}/logs",
864                self.fc.base_url, task_id
865            ))
866            .send()
867            .await?;
868        FakeCloud::parse(resp).await
869    }
870
871    /// Force the running container behind a task to stop.
872    pub async fn force_stop_task(&self, task_id: &str) -> Result<EcsTask, Error> {
873        let resp = self
874            .fc
875            .client
876            .post(format!(
877                "{}/_fakecloud/ecs/tasks/{}/force-stop",
878                self.fc.base_url, task_id
879            ))
880            .send()
881            .await?;
882        FakeCloud::parse(resp).await
883    }
884
885    /// Flip the task to STOPPED without killing the underlying container
886    /// — useful for simulating task failures in tests.
887    pub async fn mark_task_failed(
888        &self,
889        task_id: &str,
890        req: &EcsMarkFailedRequest,
891    ) -> Result<EcsTask, Error> {
892        let resp = self
893            .fc
894            .client
895            .post(format!(
896                "{}/_fakecloud/ecs/tasks/{}/mark-failed",
897                self.fc.base_url, task_id
898            ))
899            .json(req)
900            .send()
901            .await?;
902        FakeCloud::parse(resp).await
903    }
904
905    /// Replay the lifecycle event log.
906    pub async fn get_events(&self) -> Result<EcsEventsResponse, Error> {
907        let resp = self
908            .fc
909            .client
910            .get(format!("{}/_fakecloud/ecs/events", self.fc.base_url))
911            .send()
912            .await?;
913        FakeCloud::parse(resp).await
914    }
915}