1#![allow(clippy::disallowed_types)]
10#![allow(clippy::missing_panics_doc)]
13
14use async_trait::async_trait;
15use bytes::Bytes;
16use cratefield_core::{
17 Answer, AnswerValue, Calibration, Captcha, CaptchaError, Classifier, ClassifierError,
18 ClassifierProfile, Clock, Completion, Credential, Database, DbError, Decision, Defer,
19 Destination, Filed, HttpClient, HttpError, KeyValue, KvError, MailError, Mailer, Message,
20 ModelTier, Prompt, Question, RateLimitError, RateLimiter, Row, Rows, SendOutcome, Statement,
21 TextModel, TextModelError, TicketDraft, TicketState, TicketStatus, Tracker, TrackerError,
22 Verdict, validate_questions,
23};
24use futures_core::future::BoxFuture;
25use http::{Request, Response};
26use std::collections::{BTreeMap, HashMap, VecDeque};
27use std::sync::Arc;
28use std::sync::atomic::{AtomicUsize, Ordering};
29use std::time::Duration;
30
31#[allow(clippy::disallowed_types)]
33use std::sync::Mutex;
34
35#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum MailerMode {
57 SendOk,
59 NotConfigured,
61 Fail,
63 Error(MailError),
65}
66
67#[derive(Clone)]
68pub struct FakeMailer {
69 inner: Arc<FakeMailerInner>,
70}
71
72struct FakeMailerInner {
73 mode: Mutex<MailerMode>,
74 sent: Mutex<Vec<Message>>,
75}
76
77impl FakeMailer {
78 #[must_use]
79 pub fn new(mode: MailerMode) -> Self {
80 Self {
81 inner: Arc::new(FakeMailerInner {
82 mode: Mutex::new(mode),
83 sent: Mutex::new(Vec::new()),
84 }),
85 }
86 }
87
88 #[must_use]
90 pub fn sent(&self) -> Vec<Message> {
91 self.inner.sent.lock().expect("mailer lock").clone()
92 }
93
94 #[must_use]
96 pub fn last_message(&self) -> Option<Message> {
97 self.inner.sent.lock().expect("mailer lock").last().cloned()
98 }
99
100 pub fn set_mode(&self, mode: MailerMode) {
102 *self.inner.mode.lock().expect("mailer lock") = mode;
103 }
104}
105
106#[async_trait]
107impl Mailer for FakeMailer {
108 async fn send(&self, message: Message) -> Result<SendOutcome, MailError> {
109 let mode = self.inner.mode.lock().expect("mailer lock").clone();
110 match mode {
111 MailerMode::SendOk => {
112 let id = format!(
113 "fake-{}",
114 self.inner.sent.lock().expect("mailer lock").len()
115 );
116 self.inner.sent.lock().expect("mailer lock").push(message);
117 Ok(SendOutcome::Sent { id })
118 }
119 MailerMode::NotConfigured => Ok(SendOutcome::NotConfigured),
120 MailerMode::Fail => Err(MailError::Upstream("fake mailer failure".to_string())),
121 MailerMode::Error(error) => Err(error),
122 }
123 }
124}
125
126#[derive(Clone)]
130pub struct FakeCaptcha {
131 allow_all: bool,
132 allowed_tokens: Arc<Vec<String>>,
133}
134
135impl FakeCaptcha {
136 #[must_use]
138 pub fn allow_all() -> Self {
139 Self {
140 allow_all: true,
141 allowed_tokens: Arc::new(Vec::new()),
142 }
143 }
144
145 #[must_use]
147 pub fn with_tokens(tokens: impl IntoIterator<Item = impl Into<String>>) -> Self {
148 Self {
149 allow_all: false,
150 allowed_tokens: Arc::new(tokens.into_iter().map(Into::into).collect()),
151 }
152 }
153}
154
155#[async_trait]
156impl Captcha for FakeCaptcha {
157 async fn verify(&self, token: &str, _remote_ip: Option<&str>) -> Result<Verdict, CaptchaError> {
158 let ok = self.allow_all || self.allowed_tokens.iter().any(|t| t == token);
159 Ok(Verdict {
160 ok,
161 reason: (!ok).then(|| "token not allowed".to_string()),
162 })
163 }
164
165 fn binding(&self) -> Option<cratefield_core::CaptchaBinding> {
166 Some(cratefield_core::CaptchaBinding {
167 hostname_bound: true,
168 action_bound: true,
169 fail_open: false,
170 })
171 }
172}
173
174#[derive(Clone)]
178pub struct FakeRateLimiter {
179 inner: Arc<FakeRateLimiterInner>,
180}
181
182struct FakeRateLimiterInner {
183 scripted: Mutex<VecDeque<Decision>>,
184 default: Decision,
185 calls: AtomicUsize,
186}
187
188impl FakeRateLimiter {
189 #[must_use]
191 pub fn scripted(decisions: Vec<Decision>, default: Decision) -> Self {
192 Self {
193 inner: Arc::new(FakeRateLimiterInner {
194 scripted: Mutex::new(decisions.into_iter().collect()),
195 default,
196 calls: AtomicUsize::new(0),
197 }),
198 }
199 }
200
201 #[must_use]
203 pub fn always_allow() -> Self {
204 Self::scripted(
205 Vec::new(),
206 Decision {
207 ok: true,
208 retry_after: None,
209 quota: None,
210 },
211 )
212 }
213
214 #[must_use]
215 pub fn calls(&self) -> usize {
216 self.inner.calls.load(Ordering::SeqCst)
217 }
218}
219
220#[async_trait]
221impl RateLimiter for FakeRateLimiter {
222 async fn limit(&self, _key: &str) -> Result<Decision, RateLimitError> {
223 self.inner.calls.fetch_add(1, Ordering::SeqCst);
224 let scripted = self
225 .inner
226 .scripted
227 .lock()
228 .expect("limiter lock")
229 .pop_front();
230 Ok(scripted.unwrap_or_else(|| self.inner.default.clone()))
231 }
232}
233
234#[derive(Debug, Clone)]
238pub struct FixedClock(pub time::OffsetDateTime);
239
240#[async_trait]
241impl Clock for FixedClock {
242 fn now(&self) -> time::OffsetDateTime {
243 self.0
244 }
245}
246
247#[derive(Clone, Default)]
251pub struct MemoryKeyValue {
252 inner: Arc<MemoryKeyValueInner>,
253}
254
255#[derive(Default)]
256struct MemoryKeyValueInner {
257 entries: Mutex<HashMap<String, String>>,
258}
259
260impl MemoryKeyValue {
261 #[must_use]
262 pub fn new() -> Self {
263 Self::default()
264 }
265}
266
267#[async_trait]
268impl KeyValue for MemoryKeyValue {
269 async fn get(&self, key: &str) -> Result<Option<String>, KvError> {
270 Ok(self
271 .inner
272 .entries
273 .lock()
274 .expect("kv lock")
275 .get(key)
276 .cloned())
277 }
278
279 async fn put(&self, key: &str, value: &str, _ttl: Option<Duration>) -> Result<(), KvError> {
280 self.inner
281 .entries
282 .lock()
283 .expect("kv lock")
284 .insert(key.to_string(), value.to_string());
285 Ok(())
286 }
287
288 async fn delete(&self, key: &str) -> Result<(), KvError> {
289 self.inner.entries.lock().expect("kv lock").remove(key);
290 Ok(())
291 }
292}
293
294#[derive(Clone)]
298pub struct FakeHttpClient {
299 inner: Arc<FakeHttpInner>,
300}
301
302struct FakeHttpInner {
303 responses: Mutex<VecDeque<Result<Response<Bytes>, HttpError>>>,
304 captured: Mutex<Vec<(String, String, String)>>, }
306
307impl FakeHttpClient {
308 #[must_use]
310 pub fn scripted(responses: Vec<Result<Response<Bytes>, HttpError>>) -> Self {
311 Self {
312 inner: Arc::new(FakeHttpInner {
313 responses: Mutex::new(responses.into_iter().collect()),
314 captured: Mutex::new(Vec::new()),
315 }),
316 }
317 }
318
319 #[must_use]
320 pub fn ok_json(body: &'static str) -> Self {
321 Self::scripted(vec![
322 Response::builder()
323 .status(200)
324 .body(Bytes::from(body))
325 .map_err(|err| HttpError::Transport(err.to_string())),
326 ])
327 }
328
329 #[must_use]
331 pub fn captured(&self) -> Vec<(String, String, String)> {
332 self.inner.captured.lock().expect("http lock").clone()
333 }
334}
335
336#[async_trait]
337impl HttpClient for FakeHttpClient {
338 async fn send(&self, request: Request<Bytes>) -> Result<Response<Bytes>, HttpError> {
339 let (parts, body) = request.into_parts();
340 self.inner.captured.lock().expect("http lock").push((
341 parts.method.to_string(),
342 parts.uri.to_string(),
343 String::from_utf8_lossy(&body).to_string(),
344 ));
345 let next = self.inner.responses.lock().expect("http lock").pop_front();
346 next.unwrap_or_else(|| Err(HttpError::Transport("fake http exhausted".to_string())))
347 }
348}
349
350#[derive(Clone, Default)]
354pub struct FakeDefer {
355 inner: Arc<FakeDeferInner>,
356}
357
358#[derive(Default)]
359struct FakeDeferInner {
360 pending: Mutex<Vec<BoxFuture<'static, ()>>>,
361 deferred: AtomicUsize,
362}
363
364impl FakeDefer {
365 #[must_use]
366 pub fn new() -> Self {
367 Self::default()
368 }
369
370 #[allow(clippy::unused_async, clippy::unused_async_trait_impl)]
374 pub async fn drain(&self) {
375 while !self.inner.pending.lock().expect("defer lock").is_empty() {
376 let next = self.inner.pending.lock().expect("defer lock").remove(0);
377 pollster::block_on(next);
378 }
379 }
380
381 #[must_use]
382 pub fn deferred_count(&self) -> usize {
383 self.inner.deferred.load(Ordering::SeqCst)
384 }
385}
386
387impl Defer for FakeDefer {
388 fn wait_until(&self, fut: BoxFuture<'static, ()>) {
389 self.inner.deferred.fetch_add(1, Ordering::SeqCst);
390 self.inner.pending.lock().expect("defer lock").push(fut);
391 }
392}
393
394#[derive(Debug, Clone, Copy, Default)]
399pub struct EmptyDatabase;
400
401#[async_trait]
402impl Database for EmptyDatabase {
403 async fn execute(&self, stmt: &Statement) -> Result<u64, DbError> {
404 Err(DbError::Execute(format!("empty database: {}", stmt.sql)))
405 }
406
407 async fn query(&self, stmt: &Statement) -> Result<Rows, DbError> {
408 if stmt.sql.trim() == "SELECT 1" {
409 Ok(Rows::new(vec![Row::new(vec![(
410 "1".to_string(),
411 sea_query::Value::Int(Some(1)),
412 )])]))
413 } else {
414 Err(DbError::Query(format!("empty database: {}", stmt.sql)))
415 }
416 }
417
418 async fn batch_atomic(&self, _stmts: &[Statement]) -> Result<(), DbError> {
419 Err(DbError::Batch("empty database".to_string()))
420 }
421}
422
423#[derive(Clone)]
433pub struct FakeDispatcher {
434 binding: String,
435 behaviour: FakeDispatch,
436 calls: Arc<AtomicUsize>,
437}
438
439#[derive(Clone)]
440enum FakeDispatch {
441 Serve(Arc<Mutex<axum::Router>>),
442 Unbound,
443 Failing(String),
444}
445
446impl FakeDispatcher {
447 #[must_use]
449 pub fn serving(binding: impl Into<String>, router: axum::Router) -> Self {
450 Self {
451 binding: binding.into(),
452 behaviour: FakeDispatch::Serve(Arc::new(Mutex::new(router))),
453 calls: Arc::new(AtomicUsize::new(0)),
454 }
455 }
456
457 #[must_use]
460 pub fn unbound() -> Self {
461 Self {
462 binding: String::new(),
463 behaviour: FakeDispatch::Unbound,
464 calls: Arc::new(AtomicUsize::new(0)),
465 }
466 }
467
468 #[must_use]
470 pub fn failing(binding: impl Into<String>, reason: impl Into<String>) -> Self {
471 Self {
472 binding: binding.into(),
473 behaviour: FakeDispatch::Failing(reason.into()),
474 calls: Arc::new(AtomicUsize::new(0)),
475 }
476 }
477
478 #[must_use]
481 pub fn calls(&self) -> usize {
482 self.calls.load(Ordering::SeqCst)
483 }
484}
485
486#[async_trait]
487impl cratefield_core::Dispatcher for FakeDispatcher {
488 fn has(&self, binding: &str) -> bool {
489 !matches!(self.behaviour, FakeDispatch::Unbound) && binding == self.binding
490 }
491
492 async fn dispatch(
493 &self,
494 binding: &str,
495 request: Request<Bytes>,
496 ) -> Result<Response<Bytes>, cratefield_core::DispatchError> {
497 self.calls.fetch_add(1, Ordering::SeqCst);
498 match &self.behaviour {
499 FakeDispatch::Unbound => {
500 Err(cratefield_core::DispatchError::NotBound(binding.to_owned()))
501 }
502 FakeDispatch::Failing(reason) => Err(cratefield_core::DispatchError::Unavailable {
503 binding: binding.to_owned(),
504 reason: reason.clone(),
505 }),
506 FakeDispatch::Serve(router) => {
507 let router = router.lock().unwrap().clone();
508 let (parts, body) = request.into_parts();
509 let request = Request::from_parts(parts, axum::body::Body::from(body));
510 let response =
511 tower::ServiceExt::oneshot(router, request)
512 .await
513 .map_err(|err| cratefield_core::DispatchError::Unavailable {
514 binding: binding.to_owned(),
515 reason: err.to_string(),
516 })?;
517 let (parts, body) = response.into_parts();
518 let bytes = axum::body::to_bytes(body, usize::MAX)
519 .await
520 .map_err(|err| cratefield_core::DispatchError::Unavailable {
521 binding: binding.to_owned(),
522 reason: err.to_string(),
523 })?;
524 Ok(Response::from_parts(parts, bytes))
525 }
526 }
527 }
528}
529
530#[derive(Clone, Default)]
534pub struct MemoryBlob {
535 objects: Arc<std::sync::Mutex<std::collections::HashMap<String, cratefield_core::BlobObject>>>,
536}
537
538impl MemoryBlob {
539 #[must_use]
540 pub fn new() -> Self {
541 Self::default()
542 }
543
544 #[must_use]
546 pub fn len(&self) -> usize {
547 self.objects.lock().unwrap().len()
548 }
549
550 #[must_use]
552 pub fn is_empty(&self) -> bool {
553 self.len() == 0
554 }
555}
556
557#[async_trait]
558impl cratefield_core::Blob for MemoryBlob {
559 async fn put(
560 &self,
561 key: &str,
562 bytes: &[u8],
563 content_type: &str,
564 ) -> Result<(), cratefield_core::BlobError> {
565 cratefield_core::check_blob_size(bytes)?;
566 self.objects.lock().unwrap().insert(
567 key.to_owned(),
568 cratefield_core::BlobObject {
569 bytes: bytes.to_vec(),
570 content_type: content_type.to_owned(),
571 },
572 );
573 Ok(())
574 }
575 async fn get(
576 &self,
577 key: &str,
578 ) -> Result<Option<cratefield_core::BlobObject>, cratefield_core::BlobError> {
579 Ok(self.objects.lock().unwrap().get(key).cloned())
580 }
581 async fn delete(&self, key: &str) -> Result<(), cratefield_core::BlobError> {
582 self.objects.lock().unwrap().remove(key);
583 Ok(())
584 }
585 async fn signed_url(
586 &self,
587 _key: &str,
588 _ttl: std::time::Duration,
589 ) -> Result<String, cratefield_core::BlobError> {
590 Err(cratefield_core::BlobError::Unsupported(
591 "in-memory store has no presigned URLs".to_owned(),
592 ))
593 }
594}
595
596#[derive(Debug, Clone, PartialEq, Eq)]
606pub enum PushMode {
607 DeliverOk,
609 NotConfigured,
611 Unregistered,
613 Transient,
615 Error(cratefield_core::PushError),
617}
618
619#[derive(Clone)]
627pub struct FakePush {
628 inner: Arc<FakePushInner>,
629}
630
631struct FakePushInner {
632 mode: Mutex<PushMode>,
633 per_recipient: Mutex<HashMap<cratefield_core::Recipient, PushMode>>,
634 sent: Mutex<Vec<(cratefield_core::Recipient, cratefield_core::Notification)>>,
635}
636
637impl FakePush {
638 #[must_use]
639 pub fn new(mode: PushMode) -> Self {
640 Self {
641 inner: Arc::new(FakePushInner {
642 mode: Mutex::new(mode),
643 per_recipient: Mutex::new(HashMap::new()),
644 sent: Mutex::new(Vec::new()),
645 }),
646 }
647 }
648
649 #[must_use]
652 pub fn sent(&self) -> Vec<(cratefield_core::Recipient, cratefield_core::Notification)> {
653 self.inner.sent.lock().expect("push lock").clone()
654 }
655
656 #[must_use]
658 pub fn last(&self) -> Option<(cratefield_core::Recipient, cratefield_core::Notification)> {
659 self.inner.sent.lock().expect("push lock").last().cloned()
660 }
661
662 #[must_use]
664 pub fn sent_to(
665 &self,
666 recipient: &cratefield_core::Recipient,
667 ) -> Vec<cratefield_core::Notification> {
668 self.inner
669 .sent
670 .lock()
671 .expect("push lock")
672 .iter()
673 .filter(|(to, _)| to == recipient)
674 .map(|(_, notification)| notification.clone())
675 .collect()
676 }
677
678 pub fn set_mode(&self, mode: PushMode) {
681 *self.inner.mode.lock().expect("push lock") = mode;
682 }
683
684 pub fn set_mode_for(&self, recipient: &cratefield_core::Recipient, mode: PushMode) {
687 self.inner
688 .per_recipient
689 .lock()
690 .expect("push lock")
691 .insert(recipient.clone(), mode);
692 }
693
694 pub fn clear_mode_for(&self, recipient: &cratefield_core::Recipient) {
696 self.inner
697 .per_recipient
698 .lock()
699 .expect("push lock")
700 .remove(recipient);
701 }
702
703 #[must_use]
705 pub fn mode_for(&self, recipient: &cratefield_core::Recipient) -> PushMode {
706 self.inner
707 .per_recipient
708 .lock()
709 .expect("push lock")
710 .get(recipient)
711 .cloned()
712 .unwrap_or_else(|| self.inner.mode.lock().expect("push lock").clone())
713 }
714}
715
716impl Default for FakePush {
717 fn default() -> Self {
718 Self::new(PushMode::DeliverOk)
719 }
720}
721
722#[async_trait]
723impl cratefield_core::Push for FakePush {
724 async fn send(
725 &self,
726 to: &cratefield_core::Recipient,
727 notification: &cratefield_core::Notification,
728 ) -> Result<cratefield_core::PushOutcome, cratefield_core::PushError> {
729 match self.mode_for(to) {
730 PushMode::DeliverOk => {
731 let id = format!(
732 "fake-push-{}",
733 self.inner.sent.lock().expect("push lock").len()
734 );
735 self.inner
736 .sent
737 .lock()
738 .expect("push lock")
739 .push((to.clone(), notification.clone()));
740 Ok(cratefield_core::PushOutcome::Delivered { id: Some(id) })
741 }
742 PushMode::NotConfigured => Ok(cratefield_core::PushOutcome::NotConfigured),
743 PushMode::Unregistered => Err(cratefield_core::PushError::Unregistered),
744 PushMode::Transient => Err(cratefield_core::PushError::transient("fake push failure")),
745 PushMode::Error(error) => Err(error),
746 }
747 }
748}
749
750#[derive(Debug, Clone, PartialEq)]
761pub enum TextModelMode {
762 Reply(String),
765 Complete(cratefield_core::Completion),
767 NotConfigured,
769 Rejected(String),
771 Transient { retry_after: Option<Duration> },
773 Error(cratefield_core::TextModelError),
775}
776
777#[derive(Clone)]
793pub struct FakeTextModel {
794 inner: Arc<FakeTextModelInner>,
795}
796
797struct FakeTextModelInner {
798 mode: Mutex<TextModelMode>,
799 per_tier: Mutex<HashMap<ModelTier, TextModelMode>>,
800 prompts: Mutex<Vec<Prompt>>,
801}
802
803fn fake_usage(prompt: &Prompt, text: &str) -> (u64, u64) {
808 let prompt_chars = prompt.system.as_deref().map_or(0, str::len)
809 + prompt
810 .messages
811 .iter()
812 .map(|turn| turn.content.len())
813 .sum::<usize>();
814 let input = (prompt_chars / 4).max(1) as u64;
815 let output = (text.len() / 4).max(1) as u64;
816 (input, output)
817}
818
819impl FakeTextModel {
820 #[must_use]
821 pub fn new(mode: TextModelMode) -> Self {
822 Self {
823 inner: Arc::new(FakeTextModelInner {
824 mode: Mutex::new(mode),
825 per_tier: Mutex::new(HashMap::new()),
826 prompts: Mutex::new(Vec::new()),
827 }),
828 }
829 }
830
831 #[must_use]
834 pub fn prompts(&self) -> Vec<Prompt> {
835 self.inner.prompts.lock().expect("text model lock").clone()
836 }
837
838 #[must_use]
840 pub fn last(&self) -> Option<Prompt> {
841 self.inner
842 .prompts
843 .lock()
844 .expect("text model lock")
845 .last()
846 .cloned()
847 }
848
849 pub fn set_mode(&self, mode: TextModelMode) {
852 *self.inner.mode.lock().expect("text model lock") = mode;
853 }
854
855 pub fn set_mode_for(&self, tier: ModelTier, mode: TextModelMode) {
858 self.inner
859 .per_tier
860 .lock()
861 .expect("text model lock")
862 .insert(tier, mode);
863 }
864
865 pub fn clear_mode_for(&self, tier: ModelTier) {
867 self.inner
868 .per_tier
869 .lock()
870 .expect("text model lock")
871 .remove(&tier);
872 }
873
874 #[must_use]
876 pub fn mode_for(&self, tier: ModelTier) -> TextModelMode {
877 self.inner
878 .per_tier
879 .lock()
880 .expect("text model lock")
881 .get(&tier)
882 .cloned()
883 .unwrap_or_else(|| self.inner.mode.lock().expect("text model lock").clone())
884 }
885
886 fn record(&self, prompt: &Prompt) {
887 self.inner
888 .prompts
889 .lock()
890 .expect("text model lock")
891 .push(prompt.clone());
892 }
893}
894
895impl Default for FakeTextModel {
896 fn default() -> Self {
897 Self::new(TextModelMode::Reply("fake completion".to_owned()))
898 }
899}
900
901#[async_trait]
902impl TextModel for FakeTextModel {
903 async fn complete(&self, prompt: &Prompt) -> Result<Completion, TextModelError> {
904 match self.mode_for(prompt.tier) {
905 TextModelMode::Reply(text) => {
906 self.record(prompt);
907 let (input_tokens, output_tokens) = fake_usage(prompt, &text);
908 Ok(
909 Completion::new(text, format!("fake-{}", prompt.tier.name()))
910 .usage(input_tokens, output_tokens),
911 )
912 }
913 TextModelMode::Complete(completion) => {
914 self.record(prompt);
915 Ok(completion)
916 }
917 TextModelMode::NotConfigured => Err(TextModelError::NotConfigured),
918 TextModelMode::Rejected(message) => Err(TextModelError::Rejected(message)),
919 TextModelMode::Transient { retry_after } => {
920 Err(TextModelError::Transient { retry_after })
921 }
922 TextModelMode::Error(error) => Err(error),
923 }
924 }
925}
926
927#[derive(Debug, Clone, PartialEq)]
939pub enum ClassifierMode {
940 Answers(BTreeMap<String, Answer>),
944 Deterministic,
949 NotConfigured,
951 Rejected(String),
953 Transient { retry_after: Option<Duration> },
955 Error(ClassifierError),
957}
958
959#[derive(Debug, Clone, PartialEq, Eq)]
963pub struct Ask {
964 pub state: String,
966 pub question_ids: Vec<String>,
968}
969
970#[derive(Clone)]
984pub struct FakeClassifier {
985 inner: Arc<FakeClassifierInner>,
986}
987
988struct FakeClassifierInner {
989 mode: Mutex<ClassifierMode>,
990 per_question: Mutex<BTreeMap<String, Answer>>,
991 asks: Mutex<Vec<Ask>>,
992 profile: Mutex<ClassifierProfile>,
993}
994
995fn deterministic_answer(question: &Question) -> Answer {
1001 let labels = question.labels();
1002 let chosen = labels.first().copied().unwrap_or_default();
1003 let share = if labels.len() > 1 {
1006 0.4 / f32::from(u16::try_from(labels.len() - 1).unwrap_or(1))
1007 } else {
1008 0.0
1009 };
1010 let probabilities: BTreeMap<String, f32> = labels
1011 .iter()
1012 .map(|label| {
1013 let mass = if *label == chosen { 0.6 } else { share };
1014 ((*label).to_owned(), mass)
1015 })
1016 .collect();
1017 match question {
1018 Question::Choice { .. } => Answer::choice(chosen, probabilities),
1019 Question::Noul { .. } => Answer::noul(chosen == "true", probabilities),
1020 Question::Score { .. } => Answer::new(
1024 AnswerValue::Score(chosen.parse().unwrap_or_default()),
1025 probabilities,
1026 0.6,
1027 ),
1028 }
1029}
1030
1031impl FakeClassifier {
1032 #[must_use]
1033 pub fn new(mode: ClassifierMode) -> Self {
1034 Self {
1035 inner: Arc::new(FakeClassifierInner {
1036 mode: Mutex::new(mode),
1037 per_question: Mutex::new(BTreeMap::new()),
1038 asks: Mutex::new(Vec::new()),
1039 profile: Mutex::new(ClassifierProfile::new(
1040 Calibration::Classifier,
1041 cratefield_core::DEFAULT_MAX_STATE_CHARS,
1042 )),
1043 }),
1044 }
1045 }
1046
1047 #[must_use]
1050 pub fn asks(&self) -> Vec<Ask> {
1051 self.inner.asks.lock().expect("classifier lock").clone()
1052 }
1053
1054 #[must_use]
1056 pub fn last(&self) -> Option<Ask> {
1057 self.inner
1058 .asks
1059 .lock()
1060 .expect("classifier lock")
1061 .last()
1062 .cloned()
1063 }
1064
1065 pub fn set_mode(&self, mode: ClassifierMode) {
1068 *self.inner.mode.lock().expect("classifier lock") = mode;
1069 }
1070
1071 pub fn set_answer_for(&self, question_id: impl Into<String>, answer: Answer) {
1075 self.inner
1076 .per_question
1077 .lock()
1078 .expect("classifier lock")
1079 .insert(question_id.into(), answer);
1080 }
1081
1082 pub fn clear_answer_for(&self, question_id: &str) {
1084 self.inner
1085 .per_question
1086 .lock()
1087 .expect("classifier lock")
1088 .remove(question_id);
1089 }
1090
1091 pub fn set_profile(&self, calibration: Calibration, max_state_chars: usize) {
1095 *self.inner.profile.lock().expect("classifier lock") =
1096 ClassifierProfile::new(calibration, max_state_chars);
1097 }
1098}
1099
1100impl Default for FakeClassifier {
1101 fn default() -> Self {
1102 Self::new(ClassifierMode::Deterministic)
1103 }
1104}
1105
1106#[async_trait]
1107impl Classifier for FakeClassifier {
1108 fn profile(&self) -> ClassifierProfile {
1109 *self.inner.profile.lock().expect("classifier lock")
1110 }
1111
1112 async fn ask(
1113 &self,
1114 state: &str,
1115 questions: &BTreeMap<String, Question>,
1116 ) -> Result<BTreeMap<String, Answer>, ClassifierError> {
1117 validate_questions(questions)?;
1120
1121 let mode = self.inner.mode.lock().expect("classifier lock").clone();
1122 match mode {
1123 ClassifierMode::NotConfigured => return Err(ClassifierError::NotConfigured),
1124 ClassifierMode::Rejected(message) => return Err(ClassifierError::Rejected(message)),
1125 ClassifierMode::Transient { retry_after } => {
1126 return Err(ClassifierError::Transient { retry_after });
1127 }
1128 ClassifierMode::Error(error) => return Err(error),
1129 ClassifierMode::Answers(_) | ClassifierMode::Deterministic => {}
1130 }
1131
1132 let overrides = self.inner.per_question.lock().expect("classifier lock");
1133 let answers: BTreeMap<String, Answer> = questions
1134 .iter()
1135 .map(|(id, question)| {
1136 let answer = overrides.get(id).cloned().unwrap_or_else(|| match &mode {
1137 ClassifierMode::Answers(map) => map
1138 .get(id)
1139 .cloned()
1140 .unwrap_or_else(|| deterministic_answer(question)),
1141 _ => deterministic_answer(question),
1142 });
1143 (id.clone(), answer)
1144 })
1145 .collect();
1146 drop(overrides);
1147
1148 self.inner.asks.lock().expect("classifier lock").push(Ask {
1149 state: state.to_owned(),
1150 question_ids: questions.keys().cloned().collect(),
1151 });
1152 Ok(answers)
1153 }
1154}
1155
1156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1161pub enum PaymentsMode {
1162 Ok,
1164 NotConfigured,
1166 Transient,
1168}
1169
1170#[derive(Debug, Clone, PartialEq, Eq)]
1172pub enum PaymentsCall {
1173 Checkout,
1174 SubscriptionCheckout,
1175 ConnectAccountLink,
1176 ChargeWithTransfer,
1177 Refund,
1178 VerifyWebhook,
1179}
1180
1181#[derive(Clone)]
1185pub struct FakePayments {
1186 inner: Arc<FakePaymentsInner>,
1187}
1188
1189struct FakePaymentsInner {
1190 mode: Mutex<PaymentsMode>,
1191 calls: Mutex<Vec<PaymentsCall>>,
1192}
1193
1194impl FakePayments {
1195 #[must_use]
1196 pub fn new(mode: PaymentsMode) -> Self {
1197 Self {
1198 inner: Arc::new(FakePaymentsInner {
1199 mode: Mutex::new(mode),
1200 calls: Mutex::new(Vec::new()),
1201 }),
1202 }
1203 }
1204
1205 #[must_use]
1207 pub fn calls(&self) -> Vec<PaymentsCall> {
1208 self.inner.calls.lock().expect("payments lock").clone()
1209 }
1210
1211 pub fn set_mode(&self, mode: PaymentsMode) {
1212 *self.inner.mode.lock().expect("payments lock") = mode;
1213 }
1214
1215 fn record(&self, call: PaymentsCall) {
1216 self.inner.calls.lock().expect("payments lock").push(call);
1217 }
1218
1219 fn guard(&self) -> Result<(), cratefield_core::PaymentsError> {
1220 match *self.inner.mode.lock().expect("payments lock") {
1221 PaymentsMode::Ok => Ok(()),
1222 PaymentsMode::NotConfigured => Err(cratefield_core::PaymentsError::NotConfigured),
1223 PaymentsMode::Transient => Err(cratefield_core::PaymentsError::Transient(
1224 "fake payments failure".to_owned(),
1225 )),
1226 }
1227 }
1228}
1229
1230impl Default for FakePayments {
1231 fn default() -> Self {
1232 Self::new(PaymentsMode::Ok)
1233 }
1234}
1235
1236#[async_trait]
1237impl cratefield_core::Payments for FakePayments {
1238 async fn create_checkout(
1239 &self,
1240 _request: &cratefield_core::CheckoutRequest,
1241 ) -> Result<cratefield_core::CheckoutSession, cratefield_core::PaymentsError> {
1242 self.guard()?;
1243 self.record(PaymentsCall::Checkout);
1244 Ok(cratefield_core::CheckoutSession {
1245 id: "cs_fake".to_owned(),
1246 url: "https://checkout.stripe.test/cs_fake".to_owned(),
1247 })
1248 }
1249
1250 async fn create_subscription_checkout(
1251 &self,
1252 _request: &cratefield_core::SubscriptionCheckoutRequest,
1253 ) -> Result<cratefield_core::CheckoutSession, cratefield_core::PaymentsError> {
1254 self.guard()?;
1255 self.record(PaymentsCall::SubscriptionCheckout);
1256 Ok(cratefield_core::CheckoutSession {
1257 id: "cs_sub_fake".to_owned(),
1258 url: "https://checkout.stripe.test/cs_sub_fake".to_owned(),
1259 })
1260 }
1261
1262 async fn create_connect_account_link(
1263 &self,
1264 _request: &cratefield_core::ConnectAccountLinkRequest,
1265 ) -> Result<cratefield_core::ConnectAccountLink, cratefield_core::PaymentsError> {
1266 self.guard()?;
1267 self.record(PaymentsCall::ConnectAccountLink);
1268 Ok(cratefield_core::ConnectAccountLink {
1269 account_id: "acct_fake".to_owned(),
1270 url: "https://connect.stripe.test/acct_fake".to_owned(),
1271 })
1272 }
1273
1274 async fn charge_with_transfer(
1275 &self,
1276 _request: &cratefield_core::TransferCharge,
1277 ) -> Result<cratefield_core::Charge, cratefield_core::PaymentsError> {
1278 self.guard()?;
1279 self.record(PaymentsCall::ChargeWithTransfer);
1280 Ok(cratefield_core::Charge {
1281 id: "pi_fake".to_owned(),
1282 status: "succeeded".to_owned(),
1283 })
1284 }
1285
1286 async fn refund(
1287 &self,
1288 _request: &cratefield_core::RefundRequest,
1289 ) -> Result<cratefield_core::Refund, cratefield_core::PaymentsError> {
1290 self.guard()?;
1291 self.record(PaymentsCall::Refund);
1292 Ok(cratefield_core::Refund {
1293 id: "re_fake".to_owned(),
1294 })
1295 }
1296
1297 async fn verify_webhook(
1298 &self,
1299 signature_header: &str,
1300 _body: &[u8],
1301 ) -> Result<cratefield_core::WebhookEvent, cratefield_core::PaymentsError> {
1302 self.record(PaymentsCall::VerifyWebhook);
1303 if signature_header == "invalid" {
1304 return Err(cratefield_core::PaymentsError::SignatureInvalid(
1305 "fake tampered signature".to_owned(),
1306 ));
1307 }
1308 self.guard()?;
1309 Ok(cratefield_core::WebhookEvent {
1310 id: "evt_fake".to_owned(),
1311 kind: "checkout.session.completed".to_owned(),
1312 data: serde_json::json!({ "object": "checkout.session" }),
1313 })
1314 }
1315}
1316
1317fn fingerprint(credential: &Credential) -> String {
1333 use std::hash::{Hash, Hasher};
1334
1335 let mut hasher = std::hash::DefaultHasher::new();
1338 credential.expose().hash(&mut hasher);
1339 format!("fp:{:016x}", hasher.finish())
1340}
1341
1342#[derive(Debug, Clone, PartialEq, Eq)]
1351pub enum TrackerMode {
1352 FileOk,
1354 NotConfigured,
1356 Unauthorized,
1358 Rejected,
1361 Transient,
1363 Error(TrackerError),
1365}
1366
1367#[derive(Debug, Clone, PartialEq, Eq)]
1373pub struct FiledCall {
1374 pub dest: Destination,
1376 pub draft: TicketDraft,
1378 pub credential_fingerprint: String,
1380}
1381
1382#[derive(Debug, Clone, PartialEq, Eq)]
1384pub struct StatusedCall {
1385 pub dest: Destination,
1387 pub external_id: String,
1389 pub credential_fingerprint: String,
1391}
1392
1393#[derive(Clone, Debug)]
1405pub struct FakeTracker {
1406 inner: Arc<FakeTrackerInner>,
1407}
1408
1409#[derive(Debug)]
1410struct FakeTrackerInner {
1411 mode: Mutex<TrackerMode>,
1412 per_destination: Mutex<HashMap<Destination, TrackerMode>>,
1413 filed: Mutex<Vec<FiledCall>>,
1414 statused: Mutex<Vec<StatusedCall>>,
1415 state: Mutex<TicketState>,
1416}
1417
1418impl FakeTracker {
1419 #[must_use]
1420 pub fn new(mode: TrackerMode) -> Self {
1421 Self {
1422 inner: Arc::new(FakeTrackerInner {
1423 mode: Mutex::new(mode),
1424 per_destination: Mutex::new(HashMap::new()),
1425 filed: Mutex::new(Vec::new()),
1426 statused: Mutex::new(Vec::new()),
1427 state: Mutex::new(TicketState::Open),
1428 }),
1429 }
1430 }
1431
1432 #[must_use]
1434 pub fn filed(&self) -> Vec<FiledCall> {
1435 self.inner.filed.lock().expect("tracker lock").clone()
1436 }
1437
1438 #[must_use]
1440 pub fn last_filed(&self) -> Option<FiledCall> {
1441 self.inner
1442 .filed
1443 .lock()
1444 .expect("tracker lock")
1445 .last()
1446 .cloned()
1447 }
1448
1449 #[must_use]
1451 pub fn statused(&self) -> Vec<StatusedCall> {
1452 self.inner.statused.lock().expect("tracker lock").clone()
1453 }
1454
1455 pub fn set_mode(&self, mode: TrackerMode) {
1458 *self.inner.mode.lock().expect("tracker lock") = mode;
1459 }
1460
1461 pub fn set_mode_for(&self, dest: &Destination, mode: TrackerMode) {
1464 self.inner
1465 .per_destination
1466 .lock()
1467 .expect("tracker lock")
1468 .insert(dest.clone(), mode);
1469 }
1470
1471 pub fn clear_mode_for(&self, dest: &Destination) {
1474 self.inner
1475 .per_destination
1476 .lock()
1477 .expect("tracker lock")
1478 .remove(dest);
1479 }
1480
1481 #[must_use]
1483 pub fn mode_for(&self, dest: &Destination) -> TrackerMode {
1484 self.inner
1485 .per_destination
1486 .lock()
1487 .expect("tracker lock")
1488 .get(dest)
1489 .cloned()
1490 .unwrap_or_else(|| self.inner.mode.lock().expect("tracker lock").clone())
1491 }
1492
1493 pub fn set_state(&self, state: TicketState) {
1495 *self.inner.state.lock().expect("tracker lock") = state;
1496 }
1497
1498 #[must_use]
1500 pub fn state(&self) -> TicketState {
1501 *self.inner.state.lock().expect("tracker lock")
1502 }
1503
1504 fn error_for(mode: &TrackerMode) -> Option<TrackerError> {
1508 match mode {
1509 TrackerMode::FileOk => None,
1510 TrackerMode::NotConfigured => Some(TrackerError::NotConfigured),
1511 TrackerMode::Unauthorized => Some(TrackerError::Unauthorized),
1512 TrackerMode::Rejected => {
1513 Some(TrackerError::Rejected("fake tracker rejection".to_owned()))
1514 }
1515 TrackerMode::Transient => Some(TrackerError::Transient { retry_after: None }),
1516 TrackerMode::Error(error) => Some(error.clone()),
1517 }
1518 }
1519}
1520
1521impl Default for FakeTracker {
1522 fn default() -> Self {
1523 Self::new(TrackerMode::FileOk)
1524 }
1525}
1526
1527#[async_trait]
1528impl Tracker for FakeTracker {
1529 async fn file(
1530 &self,
1531 dest: &Destination,
1532 cred: &Credential,
1533 draft: &TicketDraft,
1534 ) -> Result<Filed, TrackerError> {
1535 if let Some(error) = Self::error_for(&self.mode_for(dest)) {
1536 return Err(error);
1537 }
1538 let id = format!(
1539 "fake-{}",
1540 self.inner.filed.lock().expect("tracker lock").len()
1541 );
1542 self.inner
1543 .filed
1544 .lock()
1545 .expect("tracker lock")
1546 .push(FiledCall {
1547 dest: dest.clone(),
1548 draft: draft.clone(),
1549 credential_fingerprint: fingerprint(cred),
1550 });
1551 Ok(Filed {
1552 url: format!("https://tracker.fake.test/browse/{id}"),
1553 external_id: id,
1554 })
1555 }
1556
1557 async fn status(
1558 &self,
1559 dest: &Destination,
1560 cred: &Credential,
1561 external_id: &str,
1562 ) -> Result<TicketStatus, TrackerError> {
1563 if let Some(error) = Self::error_for(&self.mode_for(dest)) {
1564 return Err(error);
1565 }
1566 self.inner
1567 .statused
1568 .lock()
1569 .expect("tracker lock")
1570 .push(StatusedCall {
1571 dest: dest.clone(),
1572 external_id: external_id.to_owned(),
1573 credential_fingerprint: fingerprint(cred),
1574 });
1575 Ok(TicketStatus {
1576 external_id: external_id.to_owned(),
1577 state: self.state(),
1578 url: None,
1579 })
1580 }
1581}
1582
1583type RealtimeMembers = std::collections::HashMap<String, Vec<String>>;
1588type RealtimeBroadcasts = Vec<(String, Vec<u8>)>;
1590
1591#[derive(Clone, Default)]
1597pub struct FakeRealtime {
1598 broadcasts: Arc<Mutex<RealtimeBroadcasts>>,
1599 members: Arc<Mutex<RealtimeMembers>>,
1600}
1601
1602impl FakeRealtime {
1603 #[must_use]
1604 pub fn new() -> Self {
1605 Self::default()
1606 }
1607
1608 pub fn set_members(&self, room_id: &str, ids: &[&str]) {
1610 self.members.lock().expect("realtime lock").insert(
1611 room_id.to_owned(),
1612 ids.iter().map(|id| (*id).to_owned()).collect(),
1613 );
1614 }
1615
1616 #[must_use]
1618 pub fn broadcasts(&self) -> Vec<(String, Vec<u8>)> {
1619 self.broadcasts.lock().expect("realtime lock").clone()
1620 }
1621}
1622
1623#[async_trait]
1624impl cratefield_core::Realtime for FakeRealtime {
1625 async fn broadcast(
1626 &self,
1627 room_id: &str,
1628 message: &[u8],
1629 ) -> Result<(), cratefield_core::RealtimeError> {
1630 self.broadcasts
1631 .lock()
1632 .expect("realtime lock")
1633 .push((room_id.to_owned(), message.to_vec()));
1634 Ok(())
1635 }
1636
1637 async fn members(
1638 &self,
1639 room_id: &str,
1640 ) -> Result<Vec<cratefield_core::Member>, cratefield_core::RealtimeError> {
1641 Ok(self
1642 .members
1643 .lock()
1644 .expect("realtime lock")
1645 .get(room_id)
1646 .map(|ids| ids.iter().map(cratefield_core::Member::new).collect())
1647 .unwrap_or_default())
1648 }
1649}
1650
1651#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1657pub enum AuthMode {
1658 Anonymous,
1661 TokenIsTheSubject,
1665 NotVerified,
1669 Unavailable,
1671}
1672
1673pub struct FakeAuth {
1676 mode: AuthMode,
1677 calls: std::sync::Mutex<Vec<String>>,
1678}
1679
1680impl FakeAuth {
1681 #[must_use]
1683 pub fn new(mode: AuthMode) -> Self {
1684 Self {
1685 mode,
1686 calls: std::sync::Mutex::new(Vec::new()),
1687 }
1688 }
1689
1690 #[must_use]
1692 pub fn subjects() -> Self {
1693 Self::new(AuthMode::TokenIsTheSubject)
1694 }
1695
1696 #[must_use]
1699 pub fn calls(&self) -> Vec<String> {
1700 self.calls.lock().expect("uncontended").clone()
1701 }
1702}
1703
1704#[async_trait::async_trait]
1705impl cratefield_core::Auth for FakeAuth {
1706 async fn identify(
1707 &self,
1708 headers: &http::HeaderMap,
1709 ) -> Result<cratefield_core::Caller, cratefield_core::AuthError> {
1710 let presented = headers
1711 .get(http::header::AUTHORIZATION)
1712 .and_then(|value| value.to_str().ok())
1713 .and_then(|value| value.strip_prefix("Bearer "))
1714 .map(str::to_owned);
1715 self.calls
1716 .lock()
1717 .expect("uncontended")
1718 .push(presented.clone().unwrap_or_default());
1719 match self.mode {
1720 AuthMode::Anonymous => Ok(cratefield_core::Caller::Anonymous),
1721 AuthMode::Unavailable => Err(cratefield_core::AuthError::Unavailable(
1722 "the fake is in Unavailable mode".to_owned(),
1723 )),
1724 AuthMode::NotVerified | AuthMode::TokenIsTheSubject => match presented {
1725 None => Ok(cratefield_core::Caller::Anonymous),
1726 Some(_) if self.mode == AuthMode::NotVerified => {
1727 Err(cratefield_core::AuthError::NotVerified)
1728 }
1729 Some(token) => Ok(cratefield_core::Caller::Subject(
1730 cratefield_core::Subject::new(token).session("fake-session"),
1731 )),
1732 },
1733 }
1734 }
1735}