1#![allow(clippy::disallowed_types)]
10#![allow(clippy::missing_panics_doc)]
13
14use async_trait::async_trait;
15use bytes::Bytes;
16use cratefield_core::{
17 Captcha, CaptchaError, Clock, Database, DbError, Decision, Defer, HttpClient, HttpError,
18 KeyValue, KvError, MailError, Mailer, Message, RateLimitError, RateLimiter, Row, Rows,
19 SendOutcome, Statement, Verdict,
20};
21use futures_core::future::BoxFuture;
22use http::{Request, Response};
23use std::collections::{HashMap, VecDeque};
24use std::sync::Arc;
25use std::sync::atomic::{AtomicUsize, Ordering};
26use std::time::Duration;
27
28#[allow(clippy::disallowed_types)]
30use std::sync::Mutex;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum MailerMode {
37 SendOk,
38 NotConfigured,
39 Fail,
40}
41
42#[derive(Clone)]
43pub struct FakeMailer {
44 inner: Arc<FakeMailerInner>,
45}
46
47struct FakeMailerInner {
48 mode: Mutex<MailerMode>,
49 sent: Mutex<Vec<Message>>,
50}
51
52impl FakeMailer {
53 #[must_use]
54 pub fn new(mode: MailerMode) -> Self {
55 Self {
56 inner: Arc::new(FakeMailerInner {
57 mode: Mutex::new(mode),
58 sent: Mutex::new(Vec::new()),
59 }),
60 }
61 }
62
63 #[must_use]
65 pub fn sent(&self) -> Vec<Message> {
66 self.inner.sent.lock().expect("mailer lock").clone()
67 }
68
69 #[must_use]
71 pub fn last_message(&self) -> Option<Message> {
72 self.inner.sent.lock().expect("mailer lock").last().cloned()
73 }
74
75 pub fn set_mode(&self, mode: MailerMode) {
77 *self.inner.mode.lock().expect("mailer lock") = mode;
78 }
79}
80
81#[async_trait]
82impl Mailer for FakeMailer {
83 async fn send(&self, message: Message) -> Result<SendOutcome, MailError> {
84 let mode = *self.inner.mode.lock().expect("mailer lock");
85 match mode {
86 MailerMode::SendOk => {
87 let id = format!(
88 "fake-{}",
89 self.inner.sent.lock().expect("mailer lock").len()
90 );
91 self.inner.sent.lock().expect("mailer lock").push(message);
92 Ok(SendOutcome::Sent { id })
93 }
94 MailerMode::NotConfigured => Ok(SendOutcome::NotConfigured),
95 MailerMode::Fail => Err(MailError::Upstream("fake mailer failure".to_string())),
96 }
97 }
98}
99
100#[derive(Clone)]
104pub struct FakeCaptcha {
105 allow_all: bool,
106 allowed_tokens: Arc<Vec<String>>,
107}
108
109impl FakeCaptcha {
110 #[must_use]
112 pub fn allow_all() -> Self {
113 Self {
114 allow_all: true,
115 allowed_tokens: Arc::new(Vec::new()),
116 }
117 }
118
119 #[must_use]
121 pub fn with_tokens(tokens: impl IntoIterator<Item = impl Into<String>>) -> Self {
122 Self {
123 allow_all: false,
124 allowed_tokens: Arc::new(tokens.into_iter().map(Into::into).collect()),
125 }
126 }
127}
128
129#[async_trait]
130impl Captcha for FakeCaptcha {
131 async fn verify(&self, token: &str, _remote_ip: Option<&str>) -> Result<Verdict, CaptchaError> {
132 let ok = self.allow_all || self.allowed_tokens.iter().any(|t| t == token);
133 Ok(Verdict {
134 ok,
135 reason: (!ok).then(|| "token not allowed".to_string()),
136 })
137 }
138}
139
140#[derive(Clone)]
144pub struct FakeRateLimiter {
145 inner: Arc<FakeRateLimiterInner>,
146}
147
148struct FakeRateLimiterInner {
149 scripted: Mutex<VecDeque<Decision>>,
150 default: Decision,
151 calls: AtomicUsize,
152}
153
154impl FakeRateLimiter {
155 #[must_use]
157 pub fn scripted(decisions: Vec<Decision>, default: Decision) -> Self {
158 Self {
159 inner: Arc::new(FakeRateLimiterInner {
160 scripted: Mutex::new(decisions.into_iter().collect()),
161 default,
162 calls: AtomicUsize::new(0),
163 }),
164 }
165 }
166
167 #[must_use]
169 pub fn always_allow() -> Self {
170 Self::scripted(
171 Vec::new(),
172 Decision {
173 ok: true,
174 retry_after: None,
175 },
176 )
177 }
178
179 #[must_use]
180 pub fn calls(&self) -> usize {
181 self.inner.calls.load(Ordering::SeqCst)
182 }
183}
184
185#[async_trait]
186impl RateLimiter for FakeRateLimiter {
187 async fn limit(&self, _key: &str) -> Result<Decision, RateLimitError> {
188 self.inner.calls.fetch_add(1, Ordering::SeqCst);
189 let scripted = self
190 .inner
191 .scripted
192 .lock()
193 .expect("limiter lock")
194 .pop_front();
195 Ok(scripted.unwrap_or_else(|| self.inner.default.clone()))
196 }
197}
198
199#[derive(Debug, Clone)]
203pub struct FixedClock(pub time::OffsetDateTime);
204
205#[async_trait]
206impl Clock for FixedClock {
207 fn now(&self) -> time::OffsetDateTime {
208 self.0
209 }
210}
211
212#[derive(Clone, Default)]
216pub struct MemoryKeyValue {
217 inner: Arc<MemoryKeyValueInner>,
218}
219
220#[derive(Default)]
221struct MemoryKeyValueInner {
222 entries: Mutex<HashMap<String, String>>,
223}
224
225impl MemoryKeyValue {
226 #[must_use]
227 pub fn new() -> Self {
228 Self::default()
229 }
230}
231
232#[async_trait]
233impl KeyValue for MemoryKeyValue {
234 async fn get(&self, key: &str) -> Result<Option<String>, KvError> {
235 Ok(self
236 .inner
237 .entries
238 .lock()
239 .expect("kv lock")
240 .get(key)
241 .cloned())
242 }
243
244 async fn put(&self, key: &str, value: &str, _ttl: Option<Duration>) -> Result<(), KvError> {
245 self.inner
246 .entries
247 .lock()
248 .expect("kv lock")
249 .insert(key.to_string(), value.to_string());
250 Ok(())
251 }
252
253 async fn delete(&self, key: &str) -> Result<(), KvError> {
254 self.inner.entries.lock().expect("kv lock").remove(key);
255 Ok(())
256 }
257}
258
259#[derive(Clone)]
263pub struct FakeHttpClient {
264 inner: Arc<FakeHttpInner>,
265}
266
267struct FakeHttpInner {
268 responses: Mutex<VecDeque<Result<Response<Bytes>, HttpError>>>,
269 captured: Mutex<Vec<(String, String, String)>>, }
271
272impl FakeHttpClient {
273 #[must_use]
275 pub fn scripted(responses: Vec<Result<Response<Bytes>, HttpError>>) -> Self {
276 Self {
277 inner: Arc::new(FakeHttpInner {
278 responses: Mutex::new(responses.into_iter().collect()),
279 captured: Mutex::new(Vec::new()),
280 }),
281 }
282 }
283
284 #[must_use]
285 pub fn ok_json(body: &'static str) -> Self {
286 Self::scripted(vec![
287 Response::builder()
288 .status(200)
289 .body(Bytes::from(body))
290 .map_err(|err| HttpError::Transport(err.to_string())),
291 ])
292 }
293
294 #[must_use]
296 pub fn captured(&self) -> Vec<(String, String, String)> {
297 self.inner.captured.lock().expect("http lock").clone()
298 }
299}
300
301#[async_trait]
302impl HttpClient for FakeHttpClient {
303 async fn send(&self, request: Request<Bytes>) -> Result<Response<Bytes>, HttpError> {
304 let (parts, body) = request.into_parts();
305 self.inner.captured.lock().expect("http lock").push((
306 parts.method.to_string(),
307 parts.uri.to_string(),
308 String::from_utf8_lossy(&body).to_string(),
309 ));
310 let next = self.inner.responses.lock().expect("http lock").pop_front();
311 next.unwrap_or_else(|| Err(HttpError::Transport("fake http exhausted".to_string())))
312 }
313}
314
315#[derive(Clone, Default)]
319pub struct FakeDefer {
320 inner: Arc<FakeDeferInner>,
321}
322
323#[derive(Default)]
324struct FakeDeferInner {
325 pending: Mutex<Vec<BoxFuture<'static, ()>>>,
326 deferred: AtomicUsize,
327}
328
329impl FakeDefer {
330 #[must_use]
331 pub fn new() -> Self {
332 Self::default()
333 }
334
335 #[allow(clippy::unused_async, clippy::unused_async_trait_impl)]
339 pub async fn drain(&self) {
340 while !self.inner.pending.lock().expect("defer lock").is_empty() {
341 let next = self.inner.pending.lock().expect("defer lock").remove(0);
342 pollster::block_on(next);
343 }
344 }
345
346 #[must_use]
347 pub fn deferred_count(&self) -> usize {
348 self.inner.deferred.load(Ordering::SeqCst)
349 }
350}
351
352impl Defer for FakeDefer {
353 fn wait_until(&self, fut: BoxFuture<'static, ()>) {
354 self.inner.deferred.fetch_add(1, Ordering::SeqCst);
355 self.inner.pending.lock().expect("defer lock").push(fut);
356 }
357}
358
359#[derive(Debug, Clone, Copy, Default)]
364pub struct EmptyDatabase;
365
366#[async_trait]
367impl Database for EmptyDatabase {
368 async fn execute(&self, stmt: &Statement) -> Result<u64, DbError> {
369 Err(DbError::Execute(format!("empty database: {}", stmt.sql)))
370 }
371
372 async fn query(&self, stmt: &Statement) -> Result<Rows, DbError> {
373 if stmt.sql.trim() == "SELECT 1" {
374 Ok(Rows::new(vec![Row::new(vec![(
375 "1".to_string(),
376 sea_query::Value::Int(Some(1)),
377 )])]))
378 } else {
379 Err(DbError::Query(format!("empty database: {}", stmt.sql)))
380 }
381 }
382
383 async fn batch(&self, _stmts: &[Statement]) -> Result<(), DbError> {
384 Err(DbError::Batch("empty database".to_string()))
385 }
386}
387
388#[derive(Clone)]
398pub struct FakeDispatcher {
399 binding: String,
400 behaviour: FakeDispatch,
401 calls: Arc<AtomicUsize>,
402}
403
404#[derive(Clone)]
405enum FakeDispatch {
406 Serve(Arc<Mutex<axum::Router>>),
407 Unbound,
408 Failing(String),
409}
410
411impl FakeDispatcher {
412 #[must_use]
414 pub fn serving(binding: impl Into<String>, router: axum::Router) -> Self {
415 Self {
416 binding: binding.into(),
417 behaviour: FakeDispatch::Serve(Arc::new(Mutex::new(router))),
418 calls: Arc::new(AtomicUsize::new(0)),
419 }
420 }
421
422 #[must_use]
425 pub fn unbound() -> Self {
426 Self {
427 binding: String::new(),
428 behaviour: FakeDispatch::Unbound,
429 calls: Arc::new(AtomicUsize::new(0)),
430 }
431 }
432
433 #[must_use]
435 pub fn failing(binding: impl Into<String>, reason: impl Into<String>) -> Self {
436 Self {
437 binding: binding.into(),
438 behaviour: FakeDispatch::Failing(reason.into()),
439 calls: Arc::new(AtomicUsize::new(0)),
440 }
441 }
442
443 #[must_use]
446 pub fn calls(&self) -> usize {
447 self.calls.load(Ordering::SeqCst)
448 }
449}
450
451#[async_trait]
452impl cratefield_core::Dispatcher for FakeDispatcher {
453 fn has(&self, binding: &str) -> bool {
454 !matches!(self.behaviour, FakeDispatch::Unbound) && binding == self.binding
455 }
456
457 async fn dispatch(
458 &self,
459 binding: &str,
460 request: Request<Bytes>,
461 ) -> Result<Response<Bytes>, cratefield_core::DispatchError> {
462 self.calls.fetch_add(1, Ordering::SeqCst);
463 match &self.behaviour {
464 FakeDispatch::Unbound => {
465 Err(cratefield_core::DispatchError::NotBound(binding.to_owned()))
466 }
467 FakeDispatch::Failing(reason) => Err(cratefield_core::DispatchError::Unavailable {
468 binding: binding.to_owned(),
469 reason: reason.clone(),
470 }),
471 FakeDispatch::Serve(router) => {
472 let router = router.lock().unwrap().clone();
473 let (parts, body) = request.into_parts();
474 let request = Request::from_parts(parts, axum::body::Body::from(body));
475 let response =
476 tower::ServiceExt::oneshot(router, request)
477 .await
478 .map_err(|err| cratefield_core::DispatchError::Unavailable {
479 binding: binding.to_owned(),
480 reason: err.to_string(),
481 })?;
482 let (parts, body) = response.into_parts();
483 let bytes = axum::body::to_bytes(body, usize::MAX)
484 .await
485 .map_err(|err| cratefield_core::DispatchError::Unavailable {
486 binding: binding.to_owned(),
487 reason: err.to_string(),
488 })?;
489 Ok(Response::from_parts(parts, bytes))
490 }
491 }
492 }
493}
494
495#[derive(Clone, Default)]
499pub struct MemoryBlob {
500 objects: Arc<std::sync::Mutex<std::collections::HashMap<String, cratefield_core::BlobObject>>>,
501}
502
503impl MemoryBlob {
504 #[must_use]
505 pub fn new() -> Self {
506 Self::default()
507 }
508
509 #[must_use]
511 pub fn len(&self) -> usize {
512 self.objects.lock().unwrap().len()
513 }
514
515 #[must_use]
517 pub fn is_empty(&self) -> bool {
518 self.len() == 0
519 }
520}
521
522#[async_trait]
523impl cratefield_core::Blob for MemoryBlob {
524 async fn put(
525 &self,
526 key: &str,
527 bytes: &[u8],
528 content_type: &str,
529 ) -> Result<(), cratefield_core::BlobError> {
530 self.objects.lock().unwrap().insert(
531 key.to_owned(),
532 cratefield_core::BlobObject {
533 bytes: bytes.to_vec(),
534 content_type: content_type.to_owned(),
535 },
536 );
537 Ok(())
538 }
539 async fn get(
540 &self,
541 key: &str,
542 ) -> Result<Option<cratefield_core::BlobObject>, cratefield_core::BlobError> {
543 Ok(self.objects.lock().unwrap().get(key).cloned())
544 }
545 async fn delete(&self, key: &str) -> Result<(), cratefield_core::BlobError> {
546 self.objects.lock().unwrap().remove(key);
547 Ok(())
548 }
549 async fn signed_url(
550 &self,
551 _key: &str,
552 _ttl: std::time::Duration,
553 ) -> Result<String, cratefield_core::BlobError> {
554 Err(cratefield_core::BlobError::Unsupported(
555 "in-memory store has no presigned URLs".to_owned(),
556 ))
557 }
558}
559
560#[derive(Debug, Clone, Copy, PartialEq, Eq)]
565pub enum PushMode {
566 DeliverOk,
568 NotConfigured,
570 Unregistered,
572 Transient,
574}
575
576#[derive(Clone)]
579pub struct FakePush {
580 inner: Arc<FakePushInner>,
581}
582
583struct FakePushInner {
584 mode: Mutex<PushMode>,
585 sent: Mutex<Vec<(String, cratefield_core::Notification)>>,
586}
587
588impl FakePush {
589 #[must_use]
590 pub fn new(mode: PushMode) -> Self {
591 Self {
592 inner: Arc::new(FakePushInner {
593 mode: Mutex::new(mode),
594 sent: Mutex::new(Vec::new()),
595 }),
596 }
597 }
598
599 #[must_use]
601 pub fn sent(&self) -> Vec<(String, cratefield_core::Notification)> {
602 self.inner.sent.lock().expect("push lock").clone()
603 }
604
605 #[must_use]
607 pub fn last(&self) -> Option<(String, cratefield_core::Notification)> {
608 self.inner.sent.lock().expect("push lock").last().cloned()
609 }
610
611 pub fn set_mode(&self, mode: PushMode) {
613 *self.inner.mode.lock().expect("push lock") = mode;
614 }
615}
616
617impl Default for FakePush {
618 fn default() -> Self {
619 Self::new(PushMode::DeliverOk)
620 }
621}
622
623#[async_trait]
624impl cratefield_core::Push for FakePush {
625 async fn send(
626 &self,
627 device_token: &str,
628 notification: &cratefield_core::Notification,
629 ) -> Result<cratefield_core::PushOutcome, cratefield_core::PushError> {
630 let mode = *self.inner.mode.lock().expect("push lock");
631 match mode {
632 PushMode::DeliverOk => {
633 let id = format!(
634 "fake-apns-{}",
635 self.inner.sent.lock().expect("push lock").len()
636 );
637 self.inner
638 .sent
639 .lock()
640 .expect("push lock")
641 .push((device_token.to_owned(), notification.clone()));
642 Ok(cratefield_core::PushOutcome::Delivered { id: Some(id) })
643 }
644 PushMode::NotConfigured => Ok(cratefield_core::PushOutcome::NotConfigured),
645 PushMode::Unregistered => Err(cratefield_core::PushError::Unregistered),
646 PushMode::Transient => Err(cratefield_core::PushError::Transient(
647 "fake push failure".to_owned(),
648 )),
649 }
650 }
651}
652
653#[derive(Debug, Clone, Copy, PartialEq, Eq)]
658pub enum PaymentsMode {
659 Ok,
661 NotConfigured,
663 Transient,
665}
666
667#[derive(Debug, Clone, PartialEq, Eq)]
669pub enum PaymentsCall {
670 Checkout,
671 SubscriptionCheckout,
672 ConnectAccountLink,
673 ChargeWithTransfer,
674 Refund,
675 VerifyWebhook,
676}
677
678#[derive(Clone)]
682pub struct FakePayments {
683 inner: Arc<FakePaymentsInner>,
684}
685
686struct FakePaymentsInner {
687 mode: Mutex<PaymentsMode>,
688 calls: Mutex<Vec<PaymentsCall>>,
689}
690
691impl FakePayments {
692 #[must_use]
693 pub fn new(mode: PaymentsMode) -> Self {
694 Self {
695 inner: Arc::new(FakePaymentsInner {
696 mode: Mutex::new(mode),
697 calls: Mutex::new(Vec::new()),
698 }),
699 }
700 }
701
702 #[must_use]
704 pub fn calls(&self) -> Vec<PaymentsCall> {
705 self.inner.calls.lock().expect("payments lock").clone()
706 }
707
708 pub fn set_mode(&self, mode: PaymentsMode) {
709 *self.inner.mode.lock().expect("payments lock") = mode;
710 }
711
712 fn record(&self, call: PaymentsCall) {
713 self.inner.calls.lock().expect("payments lock").push(call);
714 }
715
716 fn guard(&self) -> Result<(), cratefield_core::PaymentsError> {
717 match *self.inner.mode.lock().expect("payments lock") {
718 PaymentsMode::Ok => Ok(()),
719 PaymentsMode::NotConfigured => Err(cratefield_core::PaymentsError::NotConfigured),
720 PaymentsMode::Transient => Err(cratefield_core::PaymentsError::Transient(
721 "fake payments failure".to_owned(),
722 )),
723 }
724 }
725}
726
727impl Default for FakePayments {
728 fn default() -> Self {
729 Self::new(PaymentsMode::Ok)
730 }
731}
732
733#[async_trait]
734impl cratefield_core::Payments for FakePayments {
735 async fn create_checkout(
736 &self,
737 _request: &cratefield_core::CheckoutRequest,
738 ) -> Result<cratefield_core::CheckoutSession, cratefield_core::PaymentsError> {
739 self.guard()?;
740 self.record(PaymentsCall::Checkout);
741 Ok(cratefield_core::CheckoutSession {
742 id: "cs_fake".to_owned(),
743 url: "https://checkout.stripe.test/cs_fake".to_owned(),
744 })
745 }
746
747 async fn create_subscription_checkout(
748 &self,
749 _request: &cratefield_core::SubscriptionCheckoutRequest,
750 ) -> Result<cratefield_core::CheckoutSession, cratefield_core::PaymentsError> {
751 self.guard()?;
752 self.record(PaymentsCall::SubscriptionCheckout);
753 Ok(cratefield_core::CheckoutSession {
754 id: "cs_sub_fake".to_owned(),
755 url: "https://checkout.stripe.test/cs_sub_fake".to_owned(),
756 })
757 }
758
759 async fn create_connect_account_link(
760 &self,
761 _request: &cratefield_core::ConnectAccountLinkRequest,
762 ) -> Result<cratefield_core::ConnectAccountLink, cratefield_core::PaymentsError> {
763 self.guard()?;
764 self.record(PaymentsCall::ConnectAccountLink);
765 Ok(cratefield_core::ConnectAccountLink {
766 account_id: "acct_fake".to_owned(),
767 url: "https://connect.stripe.test/acct_fake".to_owned(),
768 })
769 }
770
771 async fn charge_with_transfer(
772 &self,
773 _request: &cratefield_core::TransferCharge,
774 ) -> Result<cratefield_core::Charge, cratefield_core::PaymentsError> {
775 self.guard()?;
776 self.record(PaymentsCall::ChargeWithTransfer);
777 Ok(cratefield_core::Charge {
778 id: "pi_fake".to_owned(),
779 status: "succeeded".to_owned(),
780 })
781 }
782
783 async fn refund(
784 &self,
785 _request: &cratefield_core::RefundRequest,
786 ) -> Result<cratefield_core::Refund, cratefield_core::PaymentsError> {
787 self.guard()?;
788 self.record(PaymentsCall::Refund);
789 Ok(cratefield_core::Refund {
790 id: "re_fake".to_owned(),
791 })
792 }
793
794 async fn verify_webhook(
795 &self,
796 signature_header: &str,
797 _body: &[u8],
798 ) -> Result<cratefield_core::WebhookEvent, cratefield_core::PaymentsError> {
799 self.record(PaymentsCall::VerifyWebhook);
800 if signature_header == "invalid" {
801 return Err(cratefield_core::PaymentsError::SignatureInvalid(
802 "fake tampered signature".to_owned(),
803 ));
804 }
805 self.guard()?;
806 Ok(cratefield_core::WebhookEvent {
807 id: "evt_fake".to_owned(),
808 kind: "checkout.session.completed".to_owned(),
809 data: serde_json::json!({ "object": "checkout.session" }),
810 })
811 }
812}