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    /// Reset a single service's state.
42    pub async fn reset_service(&self, service: &str) -> Result<ResetServiceResponse, Error> {
43        let resp = self
44            .client
45            .post(format!("{}/_fakecloud/reset/{}", self.base_url, service))
46            .send()
47            .await?;
48        Self::parse(resp).await
49    }
50
51    // ── Sub-clients ─────────────────────────────────────────────────
52
53    pub fn lambda(&self) -> LambdaClient<'_> {
54        LambdaClient { fc: self }
55    }
56
57    pub fn ses(&self) -> SesClient<'_> {
58        SesClient { fc: self }
59    }
60
61    pub fn sns(&self) -> SnsClient<'_> {
62        SnsClient { fc: self }
63    }
64
65    pub fn sqs(&self) -> SqsClient<'_> {
66        SqsClient { fc: self }
67    }
68
69    pub fn events(&self) -> EventsClient<'_> {
70        EventsClient { fc: self }
71    }
72
73    pub fn s3(&self) -> S3Client<'_> {
74        S3Client { fc: self }
75    }
76
77    pub fn dynamodb(&self) -> DynamoDbClient<'_> {
78        DynamoDbClient { fc: self }
79    }
80
81    pub fn secretsmanager(&self) -> SecretsManagerClient<'_> {
82        SecretsManagerClient { fc: self }
83    }
84
85    pub fn cognito(&self) -> CognitoClient<'_> {
86        CognitoClient { fc: self }
87    }
88
89    // ── Internal helpers ────────────────────────────────────────────
90
91    async fn parse<T: serde::de::DeserializeOwned>(resp: reqwest::Response) -> Result<T, Error> {
92        let status = resp.status().as_u16();
93        if !resp.status().is_success() {
94            let body = resp.text().await.unwrap_or_default();
95            return Err(Error::Api { status, body });
96        }
97        Ok(resp.json::<T>().await?)
98    }
99}
100
101// ── Lambda ──────────────────────────────────────────────────────────
102
103pub struct LambdaClient<'a> {
104    fc: &'a FakeCloud,
105}
106
107impl LambdaClient<'_> {
108    /// List recorded Lambda invocations.
109    pub async fn get_invocations(&self) -> Result<LambdaInvocationsResponse, Error> {
110        let resp = self
111            .fc
112            .client
113            .get(format!(
114                "{}/_fakecloud/lambda/invocations",
115                self.fc.base_url
116            ))
117            .send()
118            .await?;
119        FakeCloud::parse(resp).await
120    }
121
122    /// List warm (cached) Lambda containers.
123    pub async fn get_warm_containers(&self) -> Result<WarmContainersResponse, Error> {
124        let resp = self
125            .fc
126            .client
127            .get(format!(
128                "{}/_fakecloud/lambda/warm-containers",
129                self.fc.base_url
130            ))
131            .send()
132            .await?;
133        FakeCloud::parse(resp).await
134    }
135
136    /// Evict the warm container for a specific function.
137    pub async fn evict_container(
138        &self,
139        function_name: &str,
140    ) -> Result<EvictContainerResponse, Error> {
141        let resp = self
142            .fc
143            .client
144            .post(format!(
145                "{}/_fakecloud/lambda/{}/evict-container",
146                self.fc.base_url, function_name
147            ))
148            .send()
149            .await?;
150        FakeCloud::parse(resp).await
151    }
152}
153
154// ── SES ─────────────────────────────────────────────────────────────
155
156pub struct SesClient<'a> {
157    fc: &'a FakeCloud,
158}
159
160impl SesClient<'_> {
161    /// List all sent emails.
162    pub async fn get_emails(&self) -> Result<SesEmailsResponse, Error> {
163        let resp = self
164            .fc
165            .client
166            .get(format!("{}/_fakecloud/ses/emails", self.fc.base_url))
167            .send()
168            .await?;
169        FakeCloud::parse(resp).await
170    }
171
172    /// Simulate an inbound email (SES receipt rules).
173    pub async fn simulate_inbound(
174        &self,
175        req: &InboundEmailRequest,
176    ) -> Result<InboundEmailResponse, Error> {
177        let resp = self
178            .fc
179            .client
180            .post(format!("{}/_fakecloud/ses/inbound", self.fc.base_url))
181            .json(req)
182            .send()
183            .await?;
184        FakeCloud::parse(resp).await
185    }
186}
187
188// ── SNS ─────────────────────────────────────────────────────────────
189
190pub struct SnsClient<'a> {
191    fc: &'a FakeCloud,
192}
193
194impl SnsClient<'_> {
195    /// List all published SNS messages.
196    pub async fn get_messages(&self) -> Result<SnsMessagesResponse, Error> {
197        let resp = self
198            .fc
199            .client
200            .get(format!("{}/_fakecloud/sns/messages", self.fc.base_url))
201            .send()
202            .await?;
203        FakeCloud::parse(resp).await
204    }
205
206    /// List subscriptions pending confirmation.
207    pub async fn get_pending_confirmations(&self) -> Result<PendingConfirmationsResponse, Error> {
208        let resp = self
209            .fc
210            .client
211            .get(format!(
212                "{}/_fakecloud/sns/pending-confirmations",
213                self.fc.base_url
214            ))
215            .send()
216            .await?;
217        FakeCloud::parse(resp).await
218    }
219
220    /// Confirm a pending subscription.
221    pub async fn confirm_subscription(
222        &self,
223        req: &ConfirmSubscriptionRequest,
224    ) -> Result<ConfirmSubscriptionResponse, Error> {
225        let resp = self
226            .fc
227            .client
228            .post(format!(
229                "{}/_fakecloud/sns/confirm-subscription",
230                self.fc.base_url
231            ))
232            .json(req)
233            .send()
234            .await?;
235        FakeCloud::parse(resp).await
236    }
237}
238
239// ── SQS ─────────────────────────────────────────────────────────────
240
241pub struct SqsClient<'a> {
242    fc: &'a FakeCloud,
243}
244
245impl SqsClient<'_> {
246    /// List all messages across all queues.
247    pub async fn get_messages(&self) -> Result<SqsMessagesResponse, Error> {
248        let resp = self
249            .fc
250            .client
251            .get(format!("{}/_fakecloud/sqs/messages", self.fc.base_url))
252            .send()
253            .await?;
254        FakeCloud::parse(resp).await
255    }
256
257    /// Tick the message expiration processor (expire visibility-timed-out messages).
258    pub async fn tick_expiration(&self) -> Result<ExpirationTickResponse, Error> {
259        let resp = self
260            .fc
261            .client
262            .post(format!(
263                "{}/_fakecloud/sqs/expiration-processor/tick",
264                self.fc.base_url
265            ))
266            .send()
267            .await?;
268        FakeCloud::parse(resp).await
269    }
270
271    /// Force all messages in a queue to its DLQ.
272    pub async fn force_dlq(&self, queue_name: &str) -> Result<ForceDlqResponse, Error> {
273        let resp = self
274            .fc
275            .client
276            .post(format!(
277                "{}/_fakecloud/sqs/{}/force-dlq",
278                self.fc.base_url, queue_name
279            ))
280            .send()
281            .await?;
282        FakeCloud::parse(resp).await
283    }
284}
285
286// ── EventBridge ─────────────────────────────────────────────────────
287
288pub struct EventsClient<'a> {
289    fc: &'a FakeCloud,
290}
291
292impl EventsClient<'_> {
293    /// Get event history and delivery records.
294    pub async fn get_history(&self) -> Result<EventHistoryResponse, Error> {
295        let resp = self
296            .fc
297            .client
298            .get(format!("{}/_fakecloud/events/history", self.fc.base_url))
299            .send()
300            .await?;
301        FakeCloud::parse(resp).await
302    }
303
304    /// Fire a specific EventBridge rule manually.
305    pub async fn fire_rule(&self, req: &FireRuleRequest) -> Result<FireRuleResponse, Error> {
306        let resp = self
307            .fc
308            .client
309            .post(format!("{}/_fakecloud/events/fire-rule", self.fc.base_url))
310            .json(req)
311            .send()
312            .await?;
313        FakeCloud::parse(resp).await
314    }
315}
316
317// ── S3 ──────────────────────────────────────────────────────────────
318
319pub struct S3Client<'a> {
320    fc: &'a FakeCloud,
321}
322
323impl S3Client<'_> {
324    /// List S3 notification events.
325    pub async fn get_notifications(&self) -> Result<S3NotificationsResponse, Error> {
326        let resp = self
327            .fc
328            .client
329            .get(format!("{}/_fakecloud/s3/notifications", self.fc.base_url))
330            .send()
331            .await?;
332        FakeCloud::parse(resp).await
333    }
334
335    /// Tick the S3 lifecycle processor.
336    pub async fn tick_lifecycle(&self) -> Result<LifecycleTickResponse, Error> {
337        let resp = self
338            .fc
339            .client
340            .post(format!(
341                "{}/_fakecloud/s3/lifecycle-processor/tick",
342                self.fc.base_url
343            ))
344            .send()
345            .await?;
346        FakeCloud::parse(resp).await
347    }
348}
349
350// ── DynamoDB ────────────────────────────────────────────────────────
351
352pub struct DynamoDbClient<'a> {
353    fc: &'a FakeCloud,
354}
355
356impl DynamoDbClient<'_> {
357    /// Tick the DynamoDB TTL processor.
358    pub async fn tick_ttl(&self) -> Result<TtlTickResponse, Error> {
359        let resp = self
360            .fc
361            .client
362            .post(format!(
363                "{}/_fakecloud/dynamodb/ttl-processor/tick",
364                self.fc.base_url
365            ))
366            .send()
367            .await?;
368        FakeCloud::parse(resp).await
369    }
370}
371
372// ── SecretsManager ──────────────────────────────────────────────────
373
374pub struct SecretsManagerClient<'a> {
375    fc: &'a FakeCloud,
376}
377
378impl SecretsManagerClient<'_> {
379    /// Tick the SecretsManager rotation scheduler.
380    pub async fn tick_rotation(&self) -> Result<RotationTickResponse, Error> {
381        let resp = self
382            .fc
383            .client
384            .post(format!(
385                "{}/_fakecloud/secretsmanager/rotation-scheduler/tick",
386                self.fc.base_url
387            ))
388            .send()
389            .await?;
390        FakeCloud::parse(resp).await
391    }
392}
393
394// ── Cognito ─────────────────────────────────────────────────────────
395
396pub struct CognitoClient<'a> {
397    fc: &'a FakeCloud,
398}
399
400impl CognitoClient<'_> {
401    /// Get confirmation codes for a specific user.
402    pub async fn get_user_codes(
403        &self,
404        pool_id: &str,
405        username: &str,
406    ) -> Result<UserConfirmationCodes, Error> {
407        let resp = self
408            .fc
409            .client
410            .get(format!(
411                "{}/_fakecloud/cognito/confirmation-codes/{}/{}",
412                self.fc.base_url, pool_id, username
413            ))
414            .send()
415            .await?;
416        FakeCloud::parse(resp).await
417    }
418
419    /// List all confirmation codes across all pools.
420    pub async fn get_confirmation_codes(&self) -> Result<ConfirmationCodesResponse, Error> {
421        let resp = self
422            .fc
423            .client
424            .get(format!(
425                "{}/_fakecloud/cognito/confirmation-codes",
426                self.fc.base_url
427            ))
428            .send()
429            .await?;
430        FakeCloud::parse(resp).await
431    }
432
433    /// Confirm a user (bypass email/phone verification).
434    pub async fn confirm_user(
435        &self,
436        req: &ConfirmUserRequest,
437    ) -> Result<ConfirmUserResponse, Error> {
438        let resp = self
439            .fc
440            .client
441            .post(format!(
442                "{}/_fakecloud/cognito/confirm-user",
443                self.fc.base_url
444            ))
445            .json(req)
446            .send()
447            .await?;
448        // This endpoint returns 404 for missing users but still has a JSON body
449        let status = resp.status().as_u16();
450        let body: ConfirmUserResponse = resp.json().await?;
451        if status == 404 {
452            return Err(Error::Api {
453                status,
454                body: body.error.unwrap_or_default(),
455            });
456        }
457        Ok(body)
458    }
459
460    /// List all active tokens.
461    pub async fn get_tokens(&self) -> Result<TokensResponse, Error> {
462        let resp = self
463            .fc
464            .client
465            .get(format!("{}/_fakecloud/cognito/tokens", self.fc.base_url))
466            .send()
467            .await?;
468        FakeCloud::parse(resp).await
469    }
470
471    /// Expire tokens (optionally filtered by pool/user).
472    pub async fn expire_tokens(
473        &self,
474        req: &ExpireTokensRequest,
475    ) -> Result<ExpireTokensResponse, Error> {
476        let resp = self
477            .fc
478            .client
479            .post(format!(
480                "{}/_fakecloud/cognito/expire-tokens",
481                self.fc.base_url
482            ))
483            .json(req)
484            .send()
485            .await?;
486        FakeCloud::parse(resp).await
487    }
488
489    /// List auth events.
490    pub async fn get_auth_events(&self) -> Result<AuthEventsResponse, Error> {
491        let resp = self
492            .fc
493            .client
494            .get(format!(
495                "{}/_fakecloud/cognito/auth-events",
496                self.fc.base_url
497            ))
498            .send()
499            .await?;
500        FakeCloud::parse(resp).await
501    }
502}