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, PartialEq, Eq)]
53pub enum MailerMode {
54 SendOk,
56 NotConfigured,
58 Fail,
60 Error(MailError),
62}
63
64#[derive(Clone)]
65pub struct FakeMailer {
66 inner: Arc<FakeMailerInner>,
67}
68
69struct FakeMailerInner {
70 mode: Mutex<MailerMode>,
71 sent: Mutex<Vec<Message>>,
72}
73
74impl FakeMailer {
75 #[must_use]
76 pub fn new(mode: MailerMode) -> Self {
77 Self {
78 inner: Arc::new(FakeMailerInner {
79 mode: Mutex::new(mode),
80 sent: Mutex::new(Vec::new()),
81 }),
82 }
83 }
84
85 #[must_use]
87 pub fn sent(&self) -> Vec<Message> {
88 self.inner.sent.lock().expect("mailer lock").clone()
89 }
90
91 #[must_use]
93 pub fn last_message(&self) -> Option<Message> {
94 self.inner.sent.lock().expect("mailer lock").last().cloned()
95 }
96
97 pub fn set_mode(&self, mode: MailerMode) {
99 *self.inner.mode.lock().expect("mailer lock") = mode;
100 }
101}
102
103#[async_trait]
104impl Mailer for FakeMailer {
105 async fn send(&self, message: Message) -> Result<SendOutcome, MailError> {
106 let mode = self.inner.mode.lock().expect("mailer lock").clone();
107 match mode {
108 MailerMode::SendOk => {
109 let id = format!(
110 "fake-{}",
111 self.inner.sent.lock().expect("mailer lock").len()
112 );
113 self.inner.sent.lock().expect("mailer lock").push(message);
114 Ok(SendOutcome::Sent { id })
115 }
116 MailerMode::NotConfigured => Ok(SendOutcome::NotConfigured),
117 MailerMode::Fail => Err(MailError::Upstream("fake mailer failure".to_string())),
118 MailerMode::Error(error) => Err(error),
119 }
120 }
121}
122
123#[derive(Clone)]
127pub struct FakeCaptcha {
128 allow_all: bool,
129 allowed_tokens: Arc<Vec<String>>,
130}
131
132impl FakeCaptcha {
133 #[must_use]
135 pub fn allow_all() -> Self {
136 Self {
137 allow_all: true,
138 allowed_tokens: Arc::new(Vec::new()),
139 }
140 }
141
142 #[must_use]
144 pub fn with_tokens(tokens: impl IntoIterator<Item = impl Into<String>>) -> Self {
145 Self {
146 allow_all: false,
147 allowed_tokens: Arc::new(tokens.into_iter().map(Into::into).collect()),
148 }
149 }
150}
151
152#[async_trait]
153impl Captcha for FakeCaptcha {
154 async fn verify(&self, token: &str, _remote_ip: Option<&str>) -> Result<Verdict, CaptchaError> {
155 let ok = self.allow_all || self.allowed_tokens.iter().any(|t| t == token);
156 Ok(Verdict {
157 ok,
158 reason: (!ok).then(|| "token not allowed".to_string()),
159 })
160 }
161
162 fn binding(&self) -> Option<cratefield_core::CaptchaBinding> {
163 Some(cratefield_core::CaptchaBinding {
164 hostname_bound: true,
165 action_bound: true,
166 fail_open: false,
167 })
168 }
169}
170
171#[derive(Clone)]
175pub struct FakeRateLimiter {
176 inner: Arc<FakeRateLimiterInner>,
177}
178
179struct FakeRateLimiterInner {
180 scripted: Mutex<VecDeque<Decision>>,
181 default: Decision,
182 calls: AtomicUsize,
183}
184
185impl FakeRateLimiter {
186 #[must_use]
188 pub fn scripted(decisions: Vec<Decision>, default: Decision) -> Self {
189 Self {
190 inner: Arc::new(FakeRateLimiterInner {
191 scripted: Mutex::new(decisions.into_iter().collect()),
192 default,
193 calls: AtomicUsize::new(0),
194 }),
195 }
196 }
197
198 #[must_use]
200 pub fn always_allow() -> Self {
201 Self::scripted(
202 Vec::new(),
203 Decision {
204 ok: true,
205 retry_after: None,
206 },
207 )
208 }
209
210 #[must_use]
211 pub fn calls(&self) -> usize {
212 self.inner.calls.load(Ordering::SeqCst)
213 }
214}
215
216#[async_trait]
217impl RateLimiter for FakeRateLimiter {
218 async fn limit(&self, _key: &str) -> Result<Decision, RateLimitError> {
219 self.inner.calls.fetch_add(1, Ordering::SeqCst);
220 let scripted = self
221 .inner
222 .scripted
223 .lock()
224 .expect("limiter lock")
225 .pop_front();
226 Ok(scripted.unwrap_or_else(|| self.inner.default.clone()))
227 }
228}
229
230#[derive(Debug, Clone)]
234pub struct FixedClock(pub time::OffsetDateTime);
235
236#[async_trait]
237impl Clock for FixedClock {
238 fn now(&self) -> time::OffsetDateTime {
239 self.0
240 }
241}
242
243#[derive(Clone, Default)]
247pub struct MemoryKeyValue {
248 inner: Arc<MemoryKeyValueInner>,
249}
250
251#[derive(Default)]
252struct MemoryKeyValueInner {
253 entries: Mutex<HashMap<String, String>>,
254}
255
256impl MemoryKeyValue {
257 #[must_use]
258 pub fn new() -> Self {
259 Self::default()
260 }
261}
262
263#[async_trait]
264impl KeyValue for MemoryKeyValue {
265 async fn get(&self, key: &str) -> Result<Option<String>, KvError> {
266 Ok(self
267 .inner
268 .entries
269 .lock()
270 .expect("kv lock")
271 .get(key)
272 .cloned())
273 }
274
275 async fn put(&self, key: &str, value: &str, _ttl: Option<Duration>) -> Result<(), KvError> {
276 self.inner
277 .entries
278 .lock()
279 .expect("kv lock")
280 .insert(key.to_string(), value.to_string());
281 Ok(())
282 }
283
284 async fn delete(&self, key: &str) -> Result<(), KvError> {
285 self.inner.entries.lock().expect("kv lock").remove(key);
286 Ok(())
287 }
288}
289
290#[derive(Clone)]
294pub struct FakeHttpClient {
295 inner: Arc<FakeHttpInner>,
296}
297
298struct FakeHttpInner {
299 responses: Mutex<VecDeque<Result<Response<Bytes>, HttpError>>>,
300 captured: Mutex<Vec<(String, String, String)>>, }
302
303impl FakeHttpClient {
304 #[must_use]
306 pub fn scripted(responses: Vec<Result<Response<Bytes>, HttpError>>) -> Self {
307 Self {
308 inner: Arc::new(FakeHttpInner {
309 responses: Mutex::new(responses.into_iter().collect()),
310 captured: Mutex::new(Vec::new()),
311 }),
312 }
313 }
314
315 #[must_use]
316 pub fn ok_json(body: &'static str) -> Self {
317 Self::scripted(vec![
318 Response::builder()
319 .status(200)
320 .body(Bytes::from(body))
321 .map_err(|err| HttpError::Transport(err.to_string())),
322 ])
323 }
324
325 #[must_use]
327 pub fn captured(&self) -> Vec<(String, String, String)> {
328 self.inner.captured.lock().expect("http lock").clone()
329 }
330}
331
332#[async_trait]
333impl HttpClient for FakeHttpClient {
334 async fn send(&self, request: Request<Bytes>) -> Result<Response<Bytes>, HttpError> {
335 let (parts, body) = request.into_parts();
336 self.inner.captured.lock().expect("http lock").push((
337 parts.method.to_string(),
338 parts.uri.to_string(),
339 String::from_utf8_lossy(&body).to_string(),
340 ));
341 let next = self.inner.responses.lock().expect("http lock").pop_front();
342 next.unwrap_or_else(|| Err(HttpError::Transport("fake http exhausted".to_string())))
343 }
344}
345
346#[derive(Clone, Default)]
350pub struct FakeDefer {
351 inner: Arc<FakeDeferInner>,
352}
353
354#[derive(Default)]
355struct FakeDeferInner {
356 pending: Mutex<Vec<BoxFuture<'static, ()>>>,
357 deferred: AtomicUsize,
358}
359
360impl FakeDefer {
361 #[must_use]
362 pub fn new() -> Self {
363 Self::default()
364 }
365
366 #[allow(clippy::unused_async, clippy::unused_async_trait_impl)]
370 pub async fn drain(&self) {
371 while !self.inner.pending.lock().expect("defer lock").is_empty() {
372 let next = self.inner.pending.lock().expect("defer lock").remove(0);
373 pollster::block_on(next);
374 }
375 }
376
377 #[must_use]
378 pub fn deferred_count(&self) -> usize {
379 self.inner.deferred.load(Ordering::SeqCst)
380 }
381}
382
383impl Defer for FakeDefer {
384 fn wait_until(&self, fut: BoxFuture<'static, ()>) {
385 self.inner.deferred.fetch_add(1, Ordering::SeqCst);
386 self.inner.pending.lock().expect("defer lock").push(fut);
387 }
388}
389
390#[derive(Debug, Clone, Copy, Default)]
395pub struct EmptyDatabase;
396
397#[async_trait]
398impl Database for EmptyDatabase {
399 async fn execute(&self, stmt: &Statement) -> Result<u64, DbError> {
400 Err(DbError::Execute(format!("empty database: {}", stmt.sql)))
401 }
402
403 async fn query(&self, stmt: &Statement) -> Result<Rows, DbError> {
404 if stmt.sql.trim() == "SELECT 1" {
405 Ok(Rows::new(vec![Row::new(vec![(
406 "1".to_string(),
407 sea_query::Value::Int(Some(1)),
408 )])]))
409 } else {
410 Err(DbError::Query(format!("empty database: {}", stmt.sql)))
411 }
412 }
413
414 async fn batch_atomic(&self, _stmts: &[Statement]) -> Result<(), DbError> {
415 Err(DbError::Batch("empty database".to_string()))
416 }
417}
418
419#[derive(Clone)]
429pub struct FakeDispatcher {
430 binding: String,
431 behaviour: FakeDispatch,
432 calls: Arc<AtomicUsize>,
433}
434
435#[derive(Clone)]
436enum FakeDispatch {
437 Serve(Arc<Mutex<axum::Router>>),
438 Unbound,
439 Failing(String),
440}
441
442impl FakeDispatcher {
443 #[must_use]
445 pub fn serving(binding: impl Into<String>, router: axum::Router) -> Self {
446 Self {
447 binding: binding.into(),
448 behaviour: FakeDispatch::Serve(Arc::new(Mutex::new(router))),
449 calls: Arc::new(AtomicUsize::new(0)),
450 }
451 }
452
453 #[must_use]
456 pub fn unbound() -> Self {
457 Self {
458 binding: String::new(),
459 behaviour: FakeDispatch::Unbound,
460 calls: Arc::new(AtomicUsize::new(0)),
461 }
462 }
463
464 #[must_use]
466 pub fn failing(binding: impl Into<String>, reason: impl Into<String>) -> Self {
467 Self {
468 binding: binding.into(),
469 behaviour: FakeDispatch::Failing(reason.into()),
470 calls: Arc::new(AtomicUsize::new(0)),
471 }
472 }
473
474 #[must_use]
477 pub fn calls(&self) -> usize {
478 self.calls.load(Ordering::SeqCst)
479 }
480}
481
482#[async_trait]
483impl cratefield_core::Dispatcher for FakeDispatcher {
484 fn has(&self, binding: &str) -> bool {
485 !matches!(self.behaviour, FakeDispatch::Unbound) && binding == self.binding
486 }
487
488 async fn dispatch(
489 &self,
490 binding: &str,
491 request: Request<Bytes>,
492 ) -> Result<Response<Bytes>, cratefield_core::DispatchError> {
493 self.calls.fetch_add(1, Ordering::SeqCst);
494 match &self.behaviour {
495 FakeDispatch::Unbound => {
496 Err(cratefield_core::DispatchError::NotBound(binding.to_owned()))
497 }
498 FakeDispatch::Failing(reason) => Err(cratefield_core::DispatchError::Unavailable {
499 binding: binding.to_owned(),
500 reason: reason.clone(),
501 }),
502 FakeDispatch::Serve(router) => {
503 let router = router.lock().unwrap().clone();
504 let (parts, body) = request.into_parts();
505 let request = Request::from_parts(parts, axum::body::Body::from(body));
506 let response =
507 tower::ServiceExt::oneshot(router, request)
508 .await
509 .map_err(|err| cratefield_core::DispatchError::Unavailable {
510 binding: binding.to_owned(),
511 reason: err.to_string(),
512 })?;
513 let (parts, body) = response.into_parts();
514 let bytes = axum::body::to_bytes(body, usize::MAX)
515 .await
516 .map_err(|err| cratefield_core::DispatchError::Unavailable {
517 binding: binding.to_owned(),
518 reason: err.to_string(),
519 })?;
520 Ok(Response::from_parts(parts, bytes))
521 }
522 }
523 }
524}
525
526#[derive(Clone, Default)]
530pub struct MemoryBlob {
531 objects: Arc<std::sync::Mutex<std::collections::HashMap<String, cratefield_core::BlobObject>>>,
532}
533
534impl MemoryBlob {
535 #[must_use]
536 pub fn new() -> Self {
537 Self::default()
538 }
539
540 #[must_use]
542 pub fn len(&self) -> usize {
543 self.objects.lock().unwrap().len()
544 }
545
546 #[must_use]
548 pub fn is_empty(&self) -> bool {
549 self.len() == 0
550 }
551}
552
553#[async_trait]
554impl cratefield_core::Blob for MemoryBlob {
555 async fn put(
556 &self,
557 key: &str,
558 bytes: &[u8],
559 content_type: &str,
560 ) -> Result<(), cratefield_core::BlobError> {
561 cratefield_core::check_blob_size(bytes)?;
562 self.objects.lock().unwrap().insert(
563 key.to_owned(),
564 cratefield_core::BlobObject {
565 bytes: bytes.to_vec(),
566 content_type: content_type.to_owned(),
567 },
568 );
569 Ok(())
570 }
571 async fn get(
572 &self,
573 key: &str,
574 ) -> Result<Option<cratefield_core::BlobObject>, cratefield_core::BlobError> {
575 Ok(self.objects.lock().unwrap().get(key).cloned())
576 }
577 async fn delete(&self, key: &str) -> Result<(), cratefield_core::BlobError> {
578 self.objects.lock().unwrap().remove(key);
579 Ok(())
580 }
581 async fn signed_url(
582 &self,
583 _key: &str,
584 _ttl: std::time::Duration,
585 ) -> Result<String, cratefield_core::BlobError> {
586 Err(cratefield_core::BlobError::Unsupported(
587 "in-memory store has no presigned URLs".to_owned(),
588 ))
589 }
590}
591
592#[derive(Debug, Clone, PartialEq, Eq)]
602pub enum PushMode {
603 DeliverOk,
605 NotConfigured,
607 Unregistered,
609 Transient,
611 Error(cratefield_core::PushError),
613}
614
615#[derive(Clone)]
623pub struct FakePush {
624 inner: Arc<FakePushInner>,
625}
626
627struct FakePushInner {
628 mode: Mutex<PushMode>,
629 per_recipient: Mutex<HashMap<cratefield_core::Recipient, PushMode>>,
630 sent: Mutex<Vec<(cratefield_core::Recipient, cratefield_core::Notification)>>,
631}
632
633impl FakePush {
634 #[must_use]
635 pub fn new(mode: PushMode) -> Self {
636 Self {
637 inner: Arc::new(FakePushInner {
638 mode: Mutex::new(mode),
639 per_recipient: Mutex::new(HashMap::new()),
640 sent: Mutex::new(Vec::new()),
641 }),
642 }
643 }
644
645 #[must_use]
648 pub fn sent(&self) -> Vec<(cratefield_core::Recipient, cratefield_core::Notification)> {
649 self.inner.sent.lock().expect("push lock").clone()
650 }
651
652 #[must_use]
654 pub fn last(&self) -> Option<(cratefield_core::Recipient, cratefield_core::Notification)> {
655 self.inner.sent.lock().expect("push lock").last().cloned()
656 }
657
658 #[must_use]
660 pub fn sent_to(
661 &self,
662 recipient: &cratefield_core::Recipient,
663 ) -> Vec<cratefield_core::Notification> {
664 self.inner
665 .sent
666 .lock()
667 .expect("push lock")
668 .iter()
669 .filter(|(to, _)| to == recipient)
670 .map(|(_, notification)| notification.clone())
671 .collect()
672 }
673
674 pub fn set_mode(&self, mode: PushMode) {
677 *self.inner.mode.lock().expect("push lock") = mode;
678 }
679
680 pub fn set_mode_for(&self, recipient: &cratefield_core::Recipient, mode: PushMode) {
683 self.inner
684 .per_recipient
685 .lock()
686 .expect("push lock")
687 .insert(recipient.clone(), mode);
688 }
689
690 pub fn clear_mode_for(&self, recipient: &cratefield_core::Recipient) {
692 self.inner
693 .per_recipient
694 .lock()
695 .expect("push lock")
696 .remove(recipient);
697 }
698
699 #[must_use]
701 pub fn mode_for(&self, recipient: &cratefield_core::Recipient) -> PushMode {
702 self.inner
703 .per_recipient
704 .lock()
705 .expect("push lock")
706 .get(recipient)
707 .cloned()
708 .unwrap_or_else(|| self.inner.mode.lock().expect("push lock").clone())
709 }
710}
711
712impl Default for FakePush {
713 fn default() -> Self {
714 Self::new(PushMode::DeliverOk)
715 }
716}
717
718#[async_trait]
719impl cratefield_core::Push for FakePush {
720 async fn send(
721 &self,
722 to: &cratefield_core::Recipient,
723 notification: &cratefield_core::Notification,
724 ) -> Result<cratefield_core::PushOutcome, cratefield_core::PushError> {
725 match self.mode_for(to) {
726 PushMode::DeliverOk => {
727 let id = format!(
728 "fake-push-{}",
729 self.inner.sent.lock().expect("push lock").len()
730 );
731 self.inner
732 .sent
733 .lock()
734 .expect("push lock")
735 .push((to.clone(), notification.clone()));
736 Ok(cratefield_core::PushOutcome::Delivered { id: Some(id) })
737 }
738 PushMode::NotConfigured => Ok(cratefield_core::PushOutcome::NotConfigured),
739 PushMode::Unregistered => Err(cratefield_core::PushError::Unregistered),
740 PushMode::Transient => Err(cratefield_core::PushError::transient("fake push failure")),
741 PushMode::Error(error) => Err(error),
742 }
743 }
744}
745
746#[derive(Debug, Clone, Copy, PartialEq, Eq)]
751pub enum PaymentsMode {
752 Ok,
754 NotConfigured,
756 Transient,
758}
759
760#[derive(Debug, Clone, PartialEq, Eq)]
762pub enum PaymentsCall {
763 Checkout,
764 SubscriptionCheckout,
765 ConnectAccountLink,
766 ChargeWithTransfer,
767 Refund,
768 VerifyWebhook,
769}
770
771#[derive(Clone)]
775pub struct FakePayments {
776 inner: Arc<FakePaymentsInner>,
777}
778
779struct FakePaymentsInner {
780 mode: Mutex<PaymentsMode>,
781 calls: Mutex<Vec<PaymentsCall>>,
782}
783
784impl FakePayments {
785 #[must_use]
786 pub fn new(mode: PaymentsMode) -> Self {
787 Self {
788 inner: Arc::new(FakePaymentsInner {
789 mode: Mutex::new(mode),
790 calls: Mutex::new(Vec::new()),
791 }),
792 }
793 }
794
795 #[must_use]
797 pub fn calls(&self) -> Vec<PaymentsCall> {
798 self.inner.calls.lock().expect("payments lock").clone()
799 }
800
801 pub fn set_mode(&self, mode: PaymentsMode) {
802 *self.inner.mode.lock().expect("payments lock") = mode;
803 }
804
805 fn record(&self, call: PaymentsCall) {
806 self.inner.calls.lock().expect("payments lock").push(call);
807 }
808
809 fn guard(&self) -> Result<(), cratefield_core::PaymentsError> {
810 match *self.inner.mode.lock().expect("payments lock") {
811 PaymentsMode::Ok => Ok(()),
812 PaymentsMode::NotConfigured => Err(cratefield_core::PaymentsError::NotConfigured),
813 PaymentsMode::Transient => Err(cratefield_core::PaymentsError::Transient(
814 "fake payments failure".to_owned(),
815 )),
816 }
817 }
818}
819
820impl Default for FakePayments {
821 fn default() -> Self {
822 Self::new(PaymentsMode::Ok)
823 }
824}
825
826#[async_trait]
827impl cratefield_core::Payments for FakePayments {
828 async fn create_checkout(
829 &self,
830 _request: &cratefield_core::CheckoutRequest,
831 ) -> Result<cratefield_core::CheckoutSession, cratefield_core::PaymentsError> {
832 self.guard()?;
833 self.record(PaymentsCall::Checkout);
834 Ok(cratefield_core::CheckoutSession {
835 id: "cs_fake".to_owned(),
836 url: "https://checkout.stripe.test/cs_fake".to_owned(),
837 })
838 }
839
840 async fn create_subscription_checkout(
841 &self,
842 _request: &cratefield_core::SubscriptionCheckoutRequest,
843 ) -> Result<cratefield_core::CheckoutSession, cratefield_core::PaymentsError> {
844 self.guard()?;
845 self.record(PaymentsCall::SubscriptionCheckout);
846 Ok(cratefield_core::CheckoutSession {
847 id: "cs_sub_fake".to_owned(),
848 url: "https://checkout.stripe.test/cs_sub_fake".to_owned(),
849 })
850 }
851
852 async fn create_connect_account_link(
853 &self,
854 _request: &cratefield_core::ConnectAccountLinkRequest,
855 ) -> Result<cratefield_core::ConnectAccountLink, cratefield_core::PaymentsError> {
856 self.guard()?;
857 self.record(PaymentsCall::ConnectAccountLink);
858 Ok(cratefield_core::ConnectAccountLink {
859 account_id: "acct_fake".to_owned(),
860 url: "https://connect.stripe.test/acct_fake".to_owned(),
861 })
862 }
863
864 async fn charge_with_transfer(
865 &self,
866 _request: &cratefield_core::TransferCharge,
867 ) -> Result<cratefield_core::Charge, cratefield_core::PaymentsError> {
868 self.guard()?;
869 self.record(PaymentsCall::ChargeWithTransfer);
870 Ok(cratefield_core::Charge {
871 id: "pi_fake".to_owned(),
872 status: "succeeded".to_owned(),
873 })
874 }
875
876 async fn refund(
877 &self,
878 _request: &cratefield_core::RefundRequest,
879 ) -> Result<cratefield_core::Refund, cratefield_core::PaymentsError> {
880 self.guard()?;
881 self.record(PaymentsCall::Refund);
882 Ok(cratefield_core::Refund {
883 id: "re_fake".to_owned(),
884 })
885 }
886
887 async fn verify_webhook(
888 &self,
889 signature_header: &str,
890 _body: &[u8],
891 ) -> Result<cratefield_core::WebhookEvent, cratefield_core::PaymentsError> {
892 self.record(PaymentsCall::VerifyWebhook);
893 if signature_header == "invalid" {
894 return Err(cratefield_core::PaymentsError::SignatureInvalid(
895 "fake tampered signature".to_owned(),
896 ));
897 }
898 self.guard()?;
899 Ok(cratefield_core::WebhookEvent {
900 id: "evt_fake".to_owned(),
901 kind: "checkout.session.completed".to_owned(),
902 data: serde_json::json!({ "object": "checkout.session" }),
903 })
904 }
905}
906
907type RealtimeMembers = std::collections::HashMap<String, Vec<String>>;
912type RealtimeBroadcasts = Vec<(String, Vec<u8>)>;
914
915#[derive(Clone, Default)]
921pub struct FakeRealtime {
922 broadcasts: Arc<Mutex<RealtimeBroadcasts>>,
923 members: Arc<Mutex<RealtimeMembers>>,
924}
925
926impl FakeRealtime {
927 #[must_use]
928 pub fn new() -> Self {
929 Self::default()
930 }
931
932 pub fn set_members(&self, room_id: &str, ids: &[&str]) {
934 self.members.lock().expect("realtime lock").insert(
935 room_id.to_owned(),
936 ids.iter().map(|id| (*id).to_owned()).collect(),
937 );
938 }
939
940 #[must_use]
942 pub fn broadcasts(&self) -> Vec<(String, Vec<u8>)> {
943 self.broadcasts.lock().expect("realtime lock").clone()
944 }
945}
946
947#[async_trait]
948impl cratefield_core::Realtime for FakeRealtime {
949 async fn broadcast(
950 &self,
951 room_id: &str,
952 message: &[u8],
953 ) -> Result<(), cratefield_core::RealtimeError> {
954 self.broadcasts
955 .lock()
956 .expect("realtime lock")
957 .push((room_id.to_owned(), message.to_vec()));
958 Ok(())
959 }
960
961 async fn members(
962 &self,
963 room_id: &str,
964 ) -> Result<Vec<cratefield_core::Member>, cratefield_core::RealtimeError> {
965 Ok(self
966 .members
967 .lock()
968 .expect("realtime lock")
969 .get(room_id)
970 .map(|ids| ids.iter().map(cratefield_core::Member::new).collect())
971 .unwrap_or_default())
972 }
973}