1#![allow(clippy::disallowed_types)]
10#![allow(clippy::missing_panics_doc)]
13
14use async_trait::async_trait;
15use bytes::Bytes;
16use cratefield_core::sigv4::{self, Credentials, SigV4Error, SignableRequest};
17use cratefield_core::{
18 Answer, AnswerValue, Calibration, Capability, Captcha, CaptchaError, CertificateStatus,
19 Classifier, ClassifierError, ClassifierProfile, Clock, Completion, Credential, CustomHostname,
20 CustomHostnameError, CustomHostnames, Database, DbError, Decision, Defer, Destination,
21 DnsRecordType, Filed, HostnameClaim, HttpClient, HttpError, KeyValue, KvError, MailError,
22 Mailer, Message, ModelTier, Prompt, ProviderStatus, Question, RateLimitError, RateLimiter, Row,
23 Rows, SendOutcome, Statement, TextModel, TextModelError, TicketComment, TicketDraft,
24 TicketState, TicketStatus, Tracker, TrackerError, Validation, Verdict, check_hostname,
25 validate_questions,
26};
27use futures_core::future::BoxFuture;
28use http::header::{CONTENT_TYPE, RETRY_AFTER};
29use http::{HeaderValue, Request, Response, StatusCode};
30use std::collections::{BTreeMap, HashMap, VecDeque};
31use std::sync::Arc;
32use std::sync::atomic::{AtomicUsize, Ordering};
33use std::time::Duration;
34
35#[allow(clippy::disallowed_types)]
37use std::sync::Mutex;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum MailerMode {
61 SendOk,
63 NotConfigured,
65 Fail,
67 Error(MailError),
69}
70
71#[derive(Clone)]
72pub struct FakeMailer {
73 inner: Arc<FakeMailerInner>,
74}
75
76struct FakeMailerInner {
77 mode: Mutex<MailerMode>,
78 sent: Mutex<Vec<Message>>,
79}
80
81impl FakeMailer {
82 #[must_use]
83 pub fn new(mode: MailerMode) -> Self {
84 Self {
85 inner: Arc::new(FakeMailerInner {
86 mode: Mutex::new(mode),
87 sent: Mutex::new(Vec::new()),
88 }),
89 }
90 }
91
92 #[must_use]
94 pub fn sent(&self) -> Vec<Message> {
95 self.inner.sent.lock().expect("mailer lock").clone()
96 }
97
98 #[must_use]
100 pub fn last_message(&self) -> Option<Message> {
101 self.inner.sent.lock().expect("mailer lock").last().cloned()
102 }
103
104 pub fn set_mode(&self, mode: MailerMode) {
106 *self.inner.mode.lock().expect("mailer lock") = mode;
107 }
108}
109
110#[async_trait]
111impl Mailer for FakeMailer {
112 async fn send(&self, message: Message) -> Result<SendOutcome, MailError> {
113 let mode = self.inner.mode.lock().expect("mailer lock").clone();
114 match mode {
115 MailerMode::SendOk => {
116 let id = format!(
117 "fake-{}",
118 self.inner.sent.lock().expect("mailer lock").len()
119 );
120 self.inner.sent.lock().expect("mailer lock").push(message);
121 Ok(SendOutcome::Sent { id })
122 }
123 MailerMode::NotConfigured => Ok(SendOutcome::NotConfigured),
124 MailerMode::Fail => Err(MailError::Upstream("fake mailer failure".to_string())),
125 MailerMode::Error(error) => Err(error),
126 }
127 }
128}
129
130#[derive(Clone)]
134pub struct FakeCaptcha {
135 allow_all: bool,
136 allowed_tokens: Arc<Vec<String>>,
137}
138
139impl FakeCaptcha {
140 #[must_use]
142 pub fn allow_all() -> Self {
143 Self {
144 allow_all: true,
145 allowed_tokens: Arc::new(Vec::new()),
146 }
147 }
148
149 #[must_use]
151 pub fn with_tokens(tokens: impl IntoIterator<Item = impl Into<String>>) -> Self {
152 Self {
153 allow_all: false,
154 allowed_tokens: Arc::new(tokens.into_iter().map(Into::into).collect()),
155 }
156 }
157}
158
159#[async_trait]
160impl Captcha for FakeCaptcha {
161 async fn verify(&self, token: &str, _remote_ip: Option<&str>) -> Result<Verdict, CaptchaError> {
162 let ok = self.allow_all || self.allowed_tokens.iter().any(|t| t == token);
163 Ok(Verdict {
164 ok,
165 reason: (!ok).then(|| "token not allowed".to_string()),
166 })
167 }
168
169 fn binding(&self) -> Option<cratefield_core::CaptchaBinding> {
170 Some(cratefield_core::CaptchaBinding {
171 hostname_bound: true,
172 action_bound: true,
173 fail_open: false,
174 })
175 }
176}
177
178#[derive(Clone)]
182pub struct FakeRateLimiter {
183 inner: Arc<FakeRateLimiterInner>,
184}
185
186struct FakeRateLimiterInner {
187 scripted: Mutex<VecDeque<Decision>>,
188 default: Decision,
189 calls: AtomicUsize,
190}
191
192impl FakeRateLimiter {
193 #[must_use]
195 pub fn scripted(decisions: Vec<Decision>, default: Decision) -> Self {
196 Self {
197 inner: Arc::new(FakeRateLimiterInner {
198 scripted: Mutex::new(decisions.into_iter().collect()),
199 default,
200 calls: AtomicUsize::new(0),
201 }),
202 }
203 }
204
205 #[must_use]
207 pub fn always_allow() -> Self {
208 Self::scripted(
209 Vec::new(),
210 Decision {
211 ok: true,
212 retry_after: None,
213 quota: None,
214 },
215 )
216 }
217
218 #[must_use]
219 pub fn calls(&self) -> usize {
220 self.inner.calls.load(Ordering::SeqCst)
221 }
222}
223
224#[async_trait]
225impl RateLimiter for FakeRateLimiter {
226 async fn limit(&self, _key: &str) -> Result<Decision, RateLimitError> {
227 self.inner.calls.fetch_add(1, Ordering::SeqCst);
228 let scripted = self
229 .inner
230 .scripted
231 .lock()
232 .expect("limiter lock")
233 .pop_front();
234 Ok(scripted.unwrap_or_else(|| self.inner.default.clone()))
235 }
236}
237
238#[derive(Debug, Clone)]
242pub struct FixedClock(pub time::OffsetDateTime);
243
244#[async_trait]
245impl Clock for FixedClock {
246 fn now(&self) -> time::OffsetDateTime {
247 self.0
248 }
249}
250
251#[derive(Clone, Default)]
255pub struct MemoryKeyValue {
256 inner: Arc<MemoryKeyValueInner>,
257}
258
259#[derive(Default)]
260struct MemoryKeyValueInner {
261 entries: Mutex<HashMap<String, String>>,
262}
263
264impl MemoryKeyValue {
265 #[must_use]
266 pub fn new() -> Self {
267 Self::default()
268 }
269}
270
271#[async_trait]
272impl KeyValue for MemoryKeyValue {
273 async fn get(&self, key: &str) -> Result<Option<String>, KvError> {
274 Ok(self
275 .inner
276 .entries
277 .lock()
278 .expect("kv lock")
279 .get(key)
280 .cloned())
281 }
282
283 async fn put(&self, key: &str, value: &str, _ttl: Option<Duration>) -> Result<(), KvError> {
284 self.inner
285 .entries
286 .lock()
287 .expect("kv lock")
288 .insert(key.to_string(), value.to_string());
289 Ok(())
290 }
291
292 async fn delete(&self, key: &str) -> Result<(), KvError> {
293 self.inner.entries.lock().expect("kv lock").remove(key);
294 Ok(())
295 }
296}
297
298#[derive(Clone)]
302pub struct FakeHttpClient {
303 inner: Arc<FakeHttpInner>,
304}
305
306struct FakeHttpInner {
307 responses: Mutex<VecDeque<Result<Response<Bytes>, HttpError>>>,
308 captured: Mutex<Vec<(String, String, String)>>, }
310
311impl FakeHttpClient {
312 #[must_use]
314 pub fn scripted(responses: Vec<Result<Response<Bytes>, HttpError>>) -> Self {
315 Self {
316 inner: Arc::new(FakeHttpInner {
317 responses: Mutex::new(responses.into_iter().collect()),
318 captured: Mutex::new(Vec::new()),
319 }),
320 }
321 }
322
323 #[must_use]
324 pub fn ok_json(body: &'static str) -> Self {
325 Self::scripted(vec![
326 Response::builder()
327 .status(200)
328 .body(Bytes::from(body))
329 .map_err(|err| HttpError::Transport(err.to_string())),
330 ])
331 }
332
333 #[must_use]
335 pub fn owlpost_problem(status: StatusCode, title: &str, detail: &str) -> Self {
336 Self::scripted(vec![Ok(problem_json(status, title, detail))])
337 }
338
339 #[must_use]
342 pub fn owlpost_rate_limited(retry_after_secs: u64) -> Self {
343 Self::scripted(vec![Ok(with_retry_after(
344 problem_json(
345 StatusCode::TOO_MANY_REQUESTS,
346 "Too Many Requests",
347 "slow down",
348 ),
349 retry_after_secs,
350 ))])
351 }
352
353 #[must_use]
355 pub fn colonizer_error(status: StatusCode, message: &str) -> Self {
356 Self::scripted(vec![Ok(error_json(status, message))])
357 }
358
359 #[must_use]
361 pub fn colonizer_rate_limited(retry_after_secs: u64) -> Self {
362 Self::scripted(vec![Ok(with_retry_after(
363 error_json(StatusCode::TOO_MANY_REQUESTS, "rate limited"),
364 retry_after_secs,
365 ))])
366 }
367
368 #[must_use]
370 pub fn captured(&self) -> Vec<(String, String, String)> {
371 self.inner.captured.lock().expect("http lock").clone()
372 }
373}
374
375#[must_use]
381pub fn problem_json(status: StatusCode, title: &str, detail: &str) -> Response<Bytes> {
382 let body = serde_json::json!({
383 "type": "about:blank",
384 "title": title,
385 "status": status.as_u16(),
386 "detail": detail,
387 })
388 .to_string();
389 Response::builder()
390 .status(status)
391 .header(CONTENT_TYPE, "application/problem+json")
392 .body(Bytes::from(body))
393 .expect("a problem+json response builds")
394}
395
396#[must_use]
399pub fn error_json(status: StatusCode, message: &str) -> Response<Bytes> {
400 let body = serde_json::json!({ "error": message }).to_string();
401 Response::builder()
402 .status(status)
403 .header(CONTENT_TYPE, "application/json")
404 .body(Bytes::from(body))
405 .expect("an error response builds")
406}
407
408#[must_use]
411pub fn with_retry_after(mut response: Response<Bytes>, retry_after_secs: u64) -> Response<Bytes> {
412 response
413 .headers_mut()
414 .insert(RETRY_AFTER, HeaderValue::from(retry_after_secs));
415 response
416}
417
418#[async_trait]
419impl HttpClient for FakeHttpClient {
420 async fn send(&self, request: Request<Bytes>) -> Result<Response<Bytes>, HttpError> {
421 let (parts, body) = request.into_parts();
422 self.inner.captured.lock().expect("http lock").push((
423 parts.method.to_string(),
424 parts.uri.to_string(),
425 String::from_utf8_lossy(&body).to_string(),
426 ));
427 let next = self.inner.responses.lock().expect("http lock").pop_front();
428 next.unwrap_or_else(|| Err(HttpError::Transport("fake http exhausted".to_string())))
429 }
430}
431
432#[derive(Clone, Default)]
436pub struct FakeDefer {
437 inner: Arc<FakeDeferInner>,
438}
439
440#[derive(Default)]
441struct FakeDeferInner {
442 pending: Mutex<Vec<BoxFuture<'static, ()>>>,
443 deferred: AtomicUsize,
444}
445
446impl FakeDefer {
447 #[must_use]
448 pub fn new() -> Self {
449 Self::default()
450 }
451
452 #[allow(clippy::unused_async, clippy::unused_async_trait_impl)]
456 pub async fn drain(&self) {
457 while !self.inner.pending.lock().expect("defer lock").is_empty() {
458 let next = self.inner.pending.lock().expect("defer lock").remove(0);
459 pollster::block_on(next);
460 }
461 }
462
463 #[must_use]
464 pub fn deferred_count(&self) -> usize {
465 self.inner.deferred.load(Ordering::SeqCst)
466 }
467}
468
469impl Defer for FakeDefer {
470 fn wait_until(&self, fut: BoxFuture<'static, ()>) {
471 self.inner.deferred.fetch_add(1, Ordering::SeqCst);
472 self.inner.pending.lock().expect("defer lock").push(fut);
473 }
474}
475
476#[derive(Debug, Clone, Copy, Default)]
481pub struct EmptyDatabase;
482
483#[async_trait]
484impl Database for EmptyDatabase {
485 async fn execute(&self, stmt: &Statement) -> Result<u64, DbError> {
486 Err(DbError::Execute(format!("empty database: {}", stmt.sql)))
487 }
488
489 async fn query(&self, stmt: &Statement) -> Result<Rows, DbError> {
490 if stmt.sql.trim() == "SELECT 1" {
491 Ok(Rows::new(vec![Row::new(vec![(
492 "1".to_string(),
493 sea_query::Value::Int(Some(1)),
494 )])]))
495 } else {
496 Err(DbError::Query(format!("empty database: {}", stmt.sql)))
497 }
498 }
499
500 async fn batch_atomic(&self, _stmts: &[Statement]) -> Result<(), DbError> {
501 Err(DbError::Batch("empty database".to_string()))
502 }
503}
504
505#[derive(Clone)]
515pub struct FakeDispatcher {
516 binding: String,
517 behaviour: FakeDispatch,
518 calls: Arc<AtomicUsize>,
519}
520
521#[derive(Clone)]
522enum FakeDispatch {
523 Serve(Arc<Mutex<axum::Router>>),
524 Unbound,
525 Failing(String),
526}
527
528impl FakeDispatcher {
529 #[must_use]
531 pub fn serving(binding: impl Into<String>, router: axum::Router) -> Self {
532 Self {
533 binding: binding.into(),
534 behaviour: FakeDispatch::Serve(Arc::new(Mutex::new(router))),
535 calls: Arc::new(AtomicUsize::new(0)),
536 }
537 }
538
539 #[must_use]
542 pub fn unbound() -> Self {
543 Self {
544 binding: String::new(),
545 behaviour: FakeDispatch::Unbound,
546 calls: Arc::new(AtomicUsize::new(0)),
547 }
548 }
549
550 #[must_use]
552 pub fn failing(binding: impl Into<String>, reason: impl Into<String>) -> Self {
553 Self {
554 binding: binding.into(),
555 behaviour: FakeDispatch::Failing(reason.into()),
556 calls: Arc::new(AtomicUsize::new(0)),
557 }
558 }
559
560 #[must_use]
563 pub fn calls(&self) -> usize {
564 self.calls.load(Ordering::SeqCst)
565 }
566}
567
568#[async_trait]
569impl cratefield_core::Dispatcher for FakeDispatcher {
570 fn has(&self, binding: &str) -> bool {
571 !matches!(self.behaviour, FakeDispatch::Unbound) && binding == self.binding
572 }
573
574 async fn dispatch(
575 &self,
576 binding: &str,
577 request: Request<Bytes>,
578 ) -> Result<Response<Bytes>, cratefield_core::DispatchError> {
579 self.calls.fetch_add(1, Ordering::SeqCst);
580 match &self.behaviour {
581 FakeDispatch::Unbound => {
582 Err(cratefield_core::DispatchError::NotBound(binding.to_owned()))
583 }
584 FakeDispatch::Failing(reason) => Err(cratefield_core::DispatchError::Unavailable {
585 binding: binding.to_owned(),
586 reason: reason.clone(),
587 }),
588 FakeDispatch::Serve(router) => {
589 let router = router.lock().unwrap().clone();
590 let (parts, body) = request.into_parts();
591 let request = Request::from_parts(parts, axum::body::Body::from(body));
592 let response =
593 tower::ServiceExt::oneshot(router, request)
594 .await
595 .map_err(|err| cratefield_core::DispatchError::Unavailable {
596 binding: binding.to_owned(),
597 reason: err.to_string(),
598 })?;
599 let (parts, body) = response.into_parts();
600 let bytes = axum::body::to_bytes(body, usize::MAX)
601 .await
602 .map_err(|err| cratefield_core::DispatchError::Unavailable {
603 binding: binding.to_owned(),
604 reason: err.to_string(),
605 })?;
606 Ok(Response::from_parts(parts, bytes))
607 }
608 }
609 }
610}
611
612#[derive(Clone, Default)]
617pub struct MemoryBlob {
618 objects: Arc<std::sync::Mutex<std::collections::HashMap<String, cratefield_core::BlobObject>>>,
619 presign_clock: Option<Arc<dyn Clock>>,
621}
622
623const PRESIGN_ACCESS_KEY_ID: &str = "AKIATESTACCESSKEY000000";
626const PRESIGN_SECRET_ACCESS_KEY: &str = "memory-blob-presign-test-secret";
627const PRESIGN_HOST: &str = "blob.test";
628
629impl MemoryBlob {
630 #[must_use]
631 pub fn new() -> Self {
632 Self::default()
633 }
634
635 #[must_use]
639 pub fn with_presign(clock: Arc<dyn Clock>) -> Self {
640 Self {
641 presign_clock: Some(clock),
642 ..Self::default()
643 }
644 }
645
646 #[must_use]
648 pub fn len(&self) -> usize {
649 self.objects.lock().unwrap().len()
650 }
651
652 #[must_use]
654 pub fn is_empty(&self) -> bool {
655 self.len() == 0
656 }
657
658 fn presign(
661 &self,
662 method: &str,
663 key: &str,
664 headers: &[(String, String)],
665 ttl: Duration,
666 ) -> Result<String, cratefield_core::BlobError> {
667 let clock = self
668 .presign_clock
669 .as_ref()
670 .ok_or_else(|| cratefield_core::BlobError::Unsupported("no presigner".to_owned()))?;
671 let path = format!("/{key}");
672 let request = SignableRequest {
673 method,
674 host: PRESIGN_HOST,
675 path: &path,
676 query: &[],
677 headers,
678 };
679 Ok(sigv4::presign(
680 &Credentials::new(PRESIGN_ACCESS_KEY_ID, PRESIGN_SECRET_ACCESS_KEY),
681 "auto",
682 "s3",
683 &request,
684 ttl.as_secs(),
685 clock.now(),
686 ))
687 }
688
689 pub fn verify_presigned(
697 &self,
698 url: &str,
699 method: &str,
700 headers: &[(String, String)],
701 ) -> Result<String, SigV4Error> {
702 let clock = self
703 .presign_clock
704 .as_ref()
705 .ok_or_else(|| SigV4Error::Malformed("no presigning clock".to_owned()))?;
706 let credentials = Credentials::new(PRESIGN_ACCESS_KEY_ID, PRESIGN_SECRET_ACCESS_KEY);
707 sigv4::verify_presigned(
708 url,
709 method,
710 headers,
711 &credentials,
712 "auto",
713 "s3",
714 clock.now(),
715 )?;
716 url.split_once("://")
717 .and_then(|(_, rest)| rest.split_once('/'))
718 .map(|(_, path)| path.split(['?', '#']).next().unwrap_or(path).to_owned())
719 .ok_or_else(|| SigV4Error::Malformed("not an absolute URL".to_owned()))
720 }
721}
722
723#[async_trait]
724impl cratefield_core::Blob for MemoryBlob {
725 async fn put(
726 &self,
727 key: &str,
728 bytes: &[u8],
729 content_type: &str,
730 ) -> Result<(), cratefield_core::BlobError> {
731 cratefield_core::check_blob_size(bytes)?;
732 self.objects.lock().unwrap().insert(
733 key.to_owned(),
734 cratefield_core::BlobObject {
735 bytes: bytes.to_vec(),
736 content_type: content_type.to_owned(),
737 },
738 );
739 Ok(())
740 }
741 async fn get(
742 &self,
743 key: &str,
744 ) -> Result<Option<cratefield_core::BlobObject>, cratefield_core::BlobError> {
745 Ok(self.objects.lock().unwrap().get(key).cloned())
746 }
747 async fn delete(&self, key: &str) -> Result<(), cratefield_core::BlobError> {
748 self.objects.lock().unwrap().remove(key);
749 Ok(())
750 }
751 async fn signed_url(
752 &self,
753 key: &str,
754 ttl: std::time::Duration,
755 ) -> Result<String, cratefield_core::BlobError> {
756 self.presign("GET", key, &[], ttl)
757 }
758 async fn signed_put_url(
759 &self,
760 key: &str,
761 content_type: &str,
762 content_length: Option<u64>,
763 ttl: std::time::Duration,
764 ) -> Result<cratefield_core::PresignedPut, cratefield_core::BlobError> {
765 let mut headers = vec![("content-type".to_owned(), content_type.to_owned())];
766 if let Some(length) = content_length {
767 headers.push(("content-length".to_owned(), length.to_string()));
768 }
769 Ok(cratefield_core::PresignedPut {
770 url: self.presign("PUT", key, &headers, ttl)?,
771 method: "PUT",
772 headers,
773 })
774 }
775}
776
777pub const FAKE_EMBEDDER_DIMENSIONS: usize = 8;
784
785pub const FAKE_EMBEDDER_MODEL: &str = "fake-embedder";
787
788#[derive(Debug, Clone, Copy, Default)]
792pub struct FakeEmbedder;
793
794impl FakeEmbedder {
795 #[must_use]
799 pub fn vector(text: &str) -> Vec<f32> {
800 let mut out = vec![0.0; FAKE_EMBEDDER_DIMENSIONS];
801 for (index, byte) in text.bytes().enumerate() {
802 out[index % FAKE_EMBEDDER_DIMENSIONS] += f32::from(byte);
803 }
804 out
805 }
806}
807
808#[async_trait]
809impl cratefield_core::Embedder for FakeEmbedder {
810 async fn embed(
811 &self,
812 texts: &[String],
813 ) -> Result<cratefield_core::Embeddings, cratefield_core::EmbedError> {
814 let input_tokens = texts
815 .iter()
816 .map(|text| text.split_whitespace().count() as u64)
817 .sum();
818 let mut embeddings =
819 cratefield_core::Embeddings::new(FAKE_EMBEDDER_MODEL).usage(input_tokens);
820 for text in texts {
821 embeddings = embeddings.vector(Self::vector(text));
822 }
823 Ok(embeddings)
824 }
825}
826
827#[derive(Debug, Clone, PartialEq, Eq)]
837pub enum PushMode {
838 DeliverOk,
840 NotConfigured,
842 Unregistered,
844 Transient,
846 Error(cratefield_core::PushError),
848}
849
850#[derive(Clone)]
858pub struct FakePush {
859 inner: Arc<FakePushInner>,
860}
861
862struct FakePushInner {
863 mode: Mutex<PushMode>,
864 per_recipient: Mutex<HashMap<cratefield_core::Recipient, PushMode>>,
865 sent: Mutex<Vec<(cratefield_core::Recipient, cratefield_core::Notification)>>,
866}
867
868impl FakePush {
869 #[must_use]
870 pub fn new(mode: PushMode) -> Self {
871 Self {
872 inner: Arc::new(FakePushInner {
873 mode: Mutex::new(mode),
874 per_recipient: Mutex::new(HashMap::new()),
875 sent: Mutex::new(Vec::new()),
876 }),
877 }
878 }
879
880 #[must_use]
883 pub fn sent(&self) -> Vec<(cratefield_core::Recipient, cratefield_core::Notification)> {
884 self.inner.sent.lock().expect("push lock").clone()
885 }
886
887 #[must_use]
889 pub fn last(&self) -> Option<(cratefield_core::Recipient, cratefield_core::Notification)> {
890 self.inner.sent.lock().expect("push lock").last().cloned()
891 }
892
893 #[must_use]
895 pub fn sent_to(
896 &self,
897 recipient: &cratefield_core::Recipient,
898 ) -> Vec<cratefield_core::Notification> {
899 self.inner
900 .sent
901 .lock()
902 .expect("push lock")
903 .iter()
904 .filter(|(to, _)| to == recipient)
905 .map(|(_, notification)| notification.clone())
906 .collect()
907 }
908
909 pub fn set_mode(&self, mode: PushMode) {
912 *self.inner.mode.lock().expect("push lock") = mode;
913 }
914
915 pub fn set_mode_for(&self, recipient: &cratefield_core::Recipient, mode: PushMode) {
918 self.inner
919 .per_recipient
920 .lock()
921 .expect("push lock")
922 .insert(recipient.clone(), mode);
923 }
924
925 pub fn clear_mode_for(&self, recipient: &cratefield_core::Recipient) {
927 self.inner
928 .per_recipient
929 .lock()
930 .expect("push lock")
931 .remove(recipient);
932 }
933
934 #[must_use]
936 pub fn mode_for(&self, recipient: &cratefield_core::Recipient) -> PushMode {
937 self.inner
938 .per_recipient
939 .lock()
940 .expect("push lock")
941 .get(recipient)
942 .cloned()
943 .unwrap_or_else(|| self.inner.mode.lock().expect("push lock").clone())
944 }
945}
946
947impl Default for FakePush {
948 fn default() -> Self {
949 Self::new(PushMode::DeliverOk)
950 }
951}
952
953#[async_trait]
954impl cratefield_core::Push for FakePush {
955 async fn send(
956 &self,
957 to: &cratefield_core::Recipient,
958 notification: &cratefield_core::Notification,
959 ) -> Result<cratefield_core::PushOutcome, cratefield_core::PushError> {
960 match self.mode_for(to) {
961 PushMode::DeliverOk => {
962 let id = format!(
963 "fake-push-{}",
964 self.inner.sent.lock().expect("push lock").len()
965 );
966 self.inner
967 .sent
968 .lock()
969 .expect("push lock")
970 .push((to.clone(), notification.clone()));
971 Ok(cratefield_core::PushOutcome::Delivered { id: Some(id) })
972 }
973 PushMode::NotConfigured => Ok(cratefield_core::PushOutcome::NotConfigured),
974 PushMode::Unregistered => Err(cratefield_core::PushError::Unregistered),
975 PushMode::Transient => Err(cratefield_core::PushError::transient("fake push failure")),
976 PushMode::Error(error) => Err(error),
977 }
978 }
979}
980
981#[derive(Debug, Clone, PartialEq)]
992pub enum TextModelMode {
993 Reply(String),
996 Complete(cratefield_core::Completion),
998 NotConfigured,
1000 Rejected(String),
1002 Transient { retry_after: Option<Duration> },
1004 Error(cratefield_core::TextModelError),
1006}
1007
1008#[derive(Clone)]
1024pub struct FakeTextModel {
1025 inner: Arc<FakeTextModelInner>,
1026}
1027
1028struct FakeTextModelInner {
1029 mode: Mutex<TextModelMode>,
1030 per_tier: Mutex<HashMap<ModelTier, TextModelMode>>,
1031 prompts: Mutex<Vec<Prompt>>,
1032}
1033
1034fn fake_usage(prompt: &Prompt, text: &str) -> (u64, u64) {
1039 let prompt_chars = prompt.system.as_deref().map_or(0, str::len)
1040 + prompt
1041 .messages
1042 .iter()
1043 .map(|turn| turn.content.len())
1044 .sum::<usize>();
1045 let input = (prompt_chars / 4).max(1) as u64;
1046 let output = (text.len() / 4).max(1) as u64;
1047 (input, output)
1048}
1049
1050impl FakeTextModel {
1051 #[must_use]
1052 pub fn new(mode: TextModelMode) -> Self {
1053 Self {
1054 inner: Arc::new(FakeTextModelInner {
1055 mode: Mutex::new(mode),
1056 per_tier: Mutex::new(HashMap::new()),
1057 prompts: Mutex::new(Vec::new()),
1058 }),
1059 }
1060 }
1061
1062 #[must_use]
1065 pub fn prompts(&self) -> Vec<Prompt> {
1066 self.inner.prompts.lock().expect("text model lock").clone()
1067 }
1068
1069 #[must_use]
1071 pub fn last(&self) -> Option<Prompt> {
1072 self.inner
1073 .prompts
1074 .lock()
1075 .expect("text model lock")
1076 .last()
1077 .cloned()
1078 }
1079
1080 pub fn set_mode(&self, mode: TextModelMode) {
1083 *self.inner.mode.lock().expect("text model lock") = mode;
1084 }
1085
1086 pub fn set_mode_for(&self, tier: ModelTier, mode: TextModelMode) {
1089 self.inner
1090 .per_tier
1091 .lock()
1092 .expect("text model lock")
1093 .insert(tier, mode);
1094 }
1095
1096 pub fn clear_mode_for(&self, tier: ModelTier) {
1098 self.inner
1099 .per_tier
1100 .lock()
1101 .expect("text model lock")
1102 .remove(&tier);
1103 }
1104
1105 #[must_use]
1107 pub fn mode_for(&self, tier: ModelTier) -> TextModelMode {
1108 self.inner
1109 .per_tier
1110 .lock()
1111 .expect("text model lock")
1112 .get(&tier)
1113 .cloned()
1114 .unwrap_or_else(|| self.inner.mode.lock().expect("text model lock").clone())
1115 }
1116
1117 fn record(&self, prompt: &Prompt) {
1118 self.inner
1119 .prompts
1120 .lock()
1121 .expect("text model lock")
1122 .push(prompt.clone());
1123 }
1124}
1125
1126impl Default for FakeTextModel {
1127 fn default() -> Self {
1128 Self::new(TextModelMode::Reply("fake completion".to_owned()))
1129 }
1130}
1131
1132#[async_trait]
1133impl TextModel for FakeTextModel {
1134 async fn complete(&self, prompt: &Prompt) -> Result<Completion, TextModelError> {
1135 if !prompt.tools.is_empty() {
1140 return Err(TextModelError::Unsupported(Capability::Tools));
1141 }
1142 match self.mode_for(prompt.tier) {
1143 TextModelMode::Reply(text) => {
1144 self.record(prompt);
1145 let (input_tokens, output_tokens) = fake_usage(prompt, &text);
1146 Ok(
1147 Completion::new(text, format!("fake-{}", prompt.tier.name()))
1148 .usage(input_tokens, output_tokens),
1149 )
1150 }
1151 TextModelMode::Complete(completion) => {
1152 self.record(prompt);
1153 Ok(completion)
1154 }
1155 TextModelMode::NotConfigured => Err(TextModelError::NotConfigured),
1156 TextModelMode::Rejected(message) => Err(TextModelError::Rejected(message)),
1157 TextModelMode::Transient { retry_after } => {
1158 Err(TextModelError::Transient { retry_after })
1159 }
1160 TextModelMode::Error(error) => Err(error),
1161 }
1162 }
1163}
1164
1165#[derive(Debug, Clone, PartialEq)]
1177pub enum ClassifierMode {
1178 Answers(BTreeMap<String, Answer>),
1182 Deterministic,
1187 NotConfigured,
1189 Rejected(String),
1191 Transient { retry_after: Option<Duration> },
1193 Error(ClassifierError),
1195}
1196
1197#[derive(Debug, Clone, PartialEq, Eq)]
1201pub struct Ask {
1202 pub state: String,
1204 pub question_ids: Vec<String>,
1206}
1207
1208#[derive(Clone)]
1222pub struct FakeClassifier {
1223 inner: Arc<FakeClassifierInner>,
1224}
1225
1226struct FakeClassifierInner {
1227 mode: Mutex<ClassifierMode>,
1228 per_question: Mutex<BTreeMap<String, Answer>>,
1229 asks: Mutex<Vec<Ask>>,
1230 profile: Mutex<ClassifierProfile>,
1231}
1232
1233fn deterministic_answer(question: &Question) -> Answer {
1239 let labels = question.labels();
1240 let chosen = labels.first().copied().unwrap_or_default();
1241 let share = if labels.len() > 1 {
1244 0.4 / f32::from(u16::try_from(labels.len() - 1).unwrap_or(1))
1245 } else {
1246 0.0
1247 };
1248 let probabilities: BTreeMap<String, f32> = labels
1249 .iter()
1250 .map(|label| {
1251 let mass = if *label == chosen { 0.6 } else { share };
1252 ((*label).to_owned(), mass)
1253 })
1254 .collect();
1255 match question {
1256 Question::Choice { .. } => Answer::choice(chosen, probabilities),
1257 Question::Noul { .. } => Answer::noul(chosen == "true", probabilities),
1258 Question::Score { .. } => Answer::new(
1262 AnswerValue::Score(chosen.parse().unwrap_or_default()),
1263 probabilities,
1264 0.6,
1265 ),
1266 }
1267}
1268
1269impl FakeClassifier {
1270 #[must_use]
1271 pub fn new(mode: ClassifierMode) -> Self {
1272 Self {
1273 inner: Arc::new(FakeClassifierInner {
1274 mode: Mutex::new(mode),
1275 per_question: Mutex::new(BTreeMap::new()),
1276 asks: Mutex::new(Vec::new()),
1277 profile: Mutex::new(ClassifierProfile::new(
1278 Calibration::Classifier,
1279 cratefield_core::DEFAULT_MAX_STATE_CHARS,
1280 )),
1281 }),
1282 }
1283 }
1284
1285 #[must_use]
1288 pub fn asks(&self) -> Vec<Ask> {
1289 self.inner.asks.lock().expect("classifier lock").clone()
1290 }
1291
1292 #[must_use]
1294 pub fn last(&self) -> Option<Ask> {
1295 self.inner
1296 .asks
1297 .lock()
1298 .expect("classifier lock")
1299 .last()
1300 .cloned()
1301 }
1302
1303 pub fn set_mode(&self, mode: ClassifierMode) {
1306 *self.inner.mode.lock().expect("classifier lock") = mode;
1307 }
1308
1309 pub fn set_answer_for(&self, question_id: impl Into<String>, answer: Answer) {
1313 self.inner
1314 .per_question
1315 .lock()
1316 .expect("classifier lock")
1317 .insert(question_id.into(), answer);
1318 }
1319
1320 pub fn clear_answer_for(&self, question_id: &str) {
1322 self.inner
1323 .per_question
1324 .lock()
1325 .expect("classifier lock")
1326 .remove(question_id);
1327 }
1328
1329 pub fn set_profile(&self, calibration: Calibration, max_state_chars: usize) {
1333 *self.inner.profile.lock().expect("classifier lock") =
1334 ClassifierProfile::new(calibration, max_state_chars);
1335 }
1336}
1337
1338impl Default for FakeClassifier {
1339 fn default() -> Self {
1340 Self::new(ClassifierMode::Deterministic)
1341 }
1342}
1343
1344#[async_trait]
1345impl Classifier for FakeClassifier {
1346 fn profile(&self) -> ClassifierProfile {
1347 *self.inner.profile.lock().expect("classifier lock")
1348 }
1349
1350 async fn ask(
1351 &self,
1352 state: &str,
1353 questions: &BTreeMap<String, Question>,
1354 ) -> Result<BTreeMap<String, Answer>, ClassifierError> {
1355 validate_questions(questions)?;
1358
1359 let mode = self.inner.mode.lock().expect("classifier lock").clone();
1360 match mode {
1361 ClassifierMode::NotConfigured => return Err(ClassifierError::NotConfigured),
1362 ClassifierMode::Rejected(message) => return Err(ClassifierError::Rejected(message)),
1363 ClassifierMode::Transient { retry_after } => {
1364 return Err(ClassifierError::Transient { retry_after });
1365 }
1366 ClassifierMode::Error(error) => return Err(error),
1367 ClassifierMode::Answers(_) | ClassifierMode::Deterministic => {}
1368 }
1369
1370 let overrides = self.inner.per_question.lock().expect("classifier lock");
1371 let answers: BTreeMap<String, Answer> = questions
1372 .iter()
1373 .map(|(id, question)| {
1374 let answer = overrides.get(id).cloned().unwrap_or_else(|| match &mode {
1375 ClassifierMode::Answers(map) => map
1376 .get(id)
1377 .cloned()
1378 .unwrap_or_else(|| deterministic_answer(question)),
1379 _ => deterministic_answer(question),
1380 });
1381 (id.clone(), answer)
1382 })
1383 .collect();
1384 drop(overrides);
1385
1386 self.inner.asks.lock().expect("classifier lock").push(Ask {
1387 state: state.to_owned(),
1388 question_ids: questions.keys().cloned().collect(),
1389 });
1390 Ok(answers)
1391 }
1392}
1393
1394#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1399pub enum PaymentsMode {
1400 Ok,
1402 NotConfigured,
1404 Transient,
1406}
1407
1408#[derive(Debug, Clone, PartialEq, Eq)]
1410pub enum PaymentsCall {
1411 Checkout,
1412 SubscriptionCheckout,
1413 ConnectAccountLink,
1414 ChargeWithTransfer,
1415 Refund,
1416 VerifyWebhook,
1417}
1418
1419#[derive(Clone)]
1423pub struct FakePayments {
1424 inner: Arc<FakePaymentsInner>,
1425}
1426
1427struct FakePaymentsInner {
1428 mode: Mutex<PaymentsMode>,
1429 calls: Mutex<Vec<PaymentsCall>>,
1430}
1431
1432impl FakePayments {
1433 #[must_use]
1434 pub fn new(mode: PaymentsMode) -> Self {
1435 Self {
1436 inner: Arc::new(FakePaymentsInner {
1437 mode: Mutex::new(mode),
1438 calls: Mutex::new(Vec::new()),
1439 }),
1440 }
1441 }
1442
1443 #[must_use]
1445 pub fn calls(&self) -> Vec<PaymentsCall> {
1446 self.inner.calls.lock().expect("payments lock").clone()
1447 }
1448
1449 pub fn set_mode(&self, mode: PaymentsMode) {
1450 *self.inner.mode.lock().expect("payments lock") = mode;
1451 }
1452
1453 fn record(&self, call: PaymentsCall) {
1454 self.inner.calls.lock().expect("payments lock").push(call);
1455 }
1456
1457 fn guard(&self) -> Result<(), cratefield_core::PaymentsError> {
1458 match *self.inner.mode.lock().expect("payments lock") {
1459 PaymentsMode::Ok => Ok(()),
1460 PaymentsMode::NotConfigured => Err(cratefield_core::PaymentsError::NotConfigured),
1461 PaymentsMode::Transient => Err(cratefield_core::PaymentsError::Transient(
1462 "fake payments failure".to_owned(),
1463 )),
1464 }
1465 }
1466}
1467
1468impl Default for FakePayments {
1469 fn default() -> Self {
1470 Self::new(PaymentsMode::Ok)
1471 }
1472}
1473
1474#[async_trait]
1475impl cratefield_core::Payments for FakePayments {
1476 async fn create_checkout(
1477 &self,
1478 _request: &cratefield_core::CheckoutRequest,
1479 ) -> Result<cratefield_core::CheckoutSession, cratefield_core::PaymentsError> {
1480 self.guard()?;
1481 self.record(PaymentsCall::Checkout);
1482 Ok(cratefield_core::CheckoutSession {
1483 id: "cs_fake".to_owned(),
1484 url: "https://checkout.stripe.test/cs_fake".to_owned(),
1485 })
1486 }
1487
1488 async fn create_subscription_checkout(
1489 &self,
1490 _request: &cratefield_core::SubscriptionCheckoutRequest,
1491 ) -> Result<cratefield_core::CheckoutSession, cratefield_core::PaymentsError> {
1492 self.guard()?;
1493 self.record(PaymentsCall::SubscriptionCheckout);
1494 Ok(cratefield_core::CheckoutSession {
1495 id: "cs_sub_fake".to_owned(),
1496 url: "https://checkout.stripe.test/cs_sub_fake".to_owned(),
1497 })
1498 }
1499
1500 async fn create_connect_account_link(
1501 &self,
1502 _request: &cratefield_core::ConnectAccountLinkRequest,
1503 ) -> Result<cratefield_core::ConnectAccountLink, cratefield_core::PaymentsError> {
1504 self.guard()?;
1505 self.record(PaymentsCall::ConnectAccountLink);
1506 Ok(cratefield_core::ConnectAccountLink {
1507 account_id: "acct_fake".to_owned(),
1508 url: "https://connect.stripe.test/acct_fake".to_owned(),
1509 })
1510 }
1511
1512 async fn charge_with_transfer(
1513 &self,
1514 _request: &cratefield_core::TransferCharge,
1515 ) -> Result<cratefield_core::Charge, cratefield_core::PaymentsError> {
1516 self.guard()?;
1517 self.record(PaymentsCall::ChargeWithTransfer);
1518 Ok(cratefield_core::Charge {
1519 id: "pi_fake".to_owned(),
1520 status: "succeeded".to_owned(),
1521 })
1522 }
1523
1524 async fn refund(
1525 &self,
1526 _request: &cratefield_core::RefundRequest,
1527 ) -> Result<cratefield_core::Refund, cratefield_core::PaymentsError> {
1528 self.guard()?;
1529 self.record(PaymentsCall::Refund);
1530 Ok(cratefield_core::Refund {
1531 id: "re_fake".to_owned(),
1532 })
1533 }
1534
1535 async fn verify_webhook(
1536 &self,
1537 signature_header: &str,
1538 _body: &[u8],
1539 ) -> Result<cratefield_core::WebhookEvent, cratefield_core::PaymentsError> {
1540 self.record(PaymentsCall::VerifyWebhook);
1541 if signature_header == "invalid" {
1542 return Err(cratefield_core::PaymentsError::SignatureInvalid(
1543 "fake tampered signature".to_owned(),
1544 ));
1545 }
1546 self.guard()?;
1547 Ok(cratefield_core::WebhookEvent {
1548 id: "evt_fake".to_owned(),
1549 kind: "checkout.session.completed".to_owned(),
1550 data: serde_json::json!({ "object": "checkout.session" }),
1551 })
1552 }
1553}
1554
1555fn fingerprint(credential: &Credential) -> String {
1571 use std::hash::{Hash, Hasher};
1572
1573 let mut hasher = std::hash::DefaultHasher::new();
1576 credential.expose().hash(&mut hasher);
1577 format!("fp:{:016x}", hasher.finish())
1578}
1579
1580#[derive(Debug, Clone, PartialEq, Eq)]
1589pub enum TrackerMode {
1590 FileOk,
1592 NotConfigured,
1594 Unauthorized,
1596 Rejected,
1599 Transient,
1601 Error(TrackerError),
1603}
1604
1605#[derive(Debug, Clone, PartialEq, Eq)]
1611pub struct FiledCall {
1612 pub dest: Destination,
1614 pub draft: TicketDraft,
1616 pub credential_fingerprint: String,
1618}
1619
1620#[derive(Debug, Clone, PartialEq, Eq)]
1622pub struct StatusedCall {
1623 pub dest: Destination,
1625 pub external_id: String,
1627 pub credential_fingerprint: String,
1629}
1630
1631#[derive(Debug, Clone, PartialEq, Eq)]
1633pub struct CommentedCall {
1634 pub dest: Destination,
1636 pub external_id: String,
1638 pub comment: TicketComment,
1640 pub credential_fingerprint: String,
1642}
1643
1644#[derive(Clone, Debug)]
1657pub struct FakeTracker {
1658 inner: Arc<FakeTrackerInner>,
1659}
1660
1661#[derive(Debug)]
1662struct FakeTrackerInner {
1663 mode: Mutex<TrackerMode>,
1664 per_destination: Mutex<HashMap<Destination, TrackerMode>>,
1665 filed: Mutex<Vec<FiledCall>>,
1666 statused: Mutex<Vec<StatusedCall>>,
1667 commented: Mutex<Vec<CommentedCall>>,
1668 state: Mutex<TicketState>,
1669}
1670
1671impl FakeTracker {
1672 #[must_use]
1673 pub fn new(mode: TrackerMode) -> Self {
1674 Self {
1675 inner: Arc::new(FakeTrackerInner {
1676 mode: Mutex::new(mode),
1677 per_destination: Mutex::new(HashMap::new()),
1678 filed: Mutex::new(Vec::new()),
1679 statused: Mutex::new(Vec::new()),
1680 commented: Mutex::new(Vec::new()),
1681 state: Mutex::new(TicketState::Open),
1682 }),
1683 }
1684 }
1685
1686 #[must_use]
1688 pub fn filed(&self) -> Vec<FiledCall> {
1689 self.inner.filed.lock().expect("tracker lock").clone()
1690 }
1691
1692 #[must_use]
1694 pub fn last_filed(&self) -> Option<FiledCall> {
1695 self.inner
1696 .filed
1697 .lock()
1698 .expect("tracker lock")
1699 .last()
1700 .cloned()
1701 }
1702
1703 #[must_use]
1705 pub fn statused(&self) -> Vec<StatusedCall> {
1706 self.inner.statused.lock().expect("tracker lock").clone()
1707 }
1708
1709 #[must_use]
1711 pub fn commented(&self) -> Vec<CommentedCall> {
1712 self.inner.commented.lock().expect("tracker lock").clone()
1713 }
1714
1715 pub fn set_mode(&self, mode: TrackerMode) {
1718 *self.inner.mode.lock().expect("tracker lock") = mode;
1719 }
1720
1721 pub fn set_mode_for(&self, dest: &Destination, mode: TrackerMode) {
1724 self.inner
1725 .per_destination
1726 .lock()
1727 .expect("tracker lock")
1728 .insert(dest.clone(), mode);
1729 }
1730
1731 pub fn clear_mode_for(&self, dest: &Destination) {
1734 self.inner
1735 .per_destination
1736 .lock()
1737 .expect("tracker lock")
1738 .remove(dest);
1739 }
1740
1741 #[must_use]
1743 pub fn mode_for(&self, dest: &Destination) -> TrackerMode {
1744 self.inner
1745 .per_destination
1746 .lock()
1747 .expect("tracker lock")
1748 .get(dest)
1749 .cloned()
1750 .unwrap_or_else(|| self.inner.mode.lock().expect("tracker lock").clone())
1751 }
1752
1753 pub fn set_state(&self, state: TicketState) {
1755 *self.inner.state.lock().expect("tracker lock") = state;
1756 }
1757
1758 #[must_use]
1760 pub fn state(&self) -> TicketState {
1761 *self.inner.state.lock().expect("tracker lock")
1762 }
1763
1764 fn error_for(mode: &TrackerMode) -> Option<TrackerError> {
1768 match mode {
1769 TrackerMode::FileOk => None,
1770 TrackerMode::NotConfigured => Some(TrackerError::NotConfigured),
1771 TrackerMode::Unauthorized => Some(TrackerError::Unauthorized),
1772 TrackerMode::Rejected => {
1773 Some(TrackerError::Rejected("fake tracker rejection".to_owned()))
1774 }
1775 TrackerMode::Transient => Some(TrackerError::Transient { retry_after: None }),
1776 TrackerMode::Error(error) => Some(error.clone()),
1777 }
1778 }
1779}
1780
1781impl Default for FakeTracker {
1782 fn default() -> Self {
1783 Self::new(TrackerMode::FileOk)
1784 }
1785}
1786
1787#[async_trait]
1788impl Tracker for FakeTracker {
1789 async fn file(
1790 &self,
1791 dest: &Destination,
1792 cred: &Credential,
1793 draft: &TicketDraft,
1794 ) -> Result<Filed, TrackerError> {
1795 if let Some(error) = Self::error_for(&self.mode_for(dest)) {
1796 return Err(error);
1797 }
1798 let id = format!(
1799 "fake-{}",
1800 self.inner.filed.lock().expect("tracker lock").len()
1801 );
1802 self.inner
1803 .filed
1804 .lock()
1805 .expect("tracker lock")
1806 .push(FiledCall {
1807 dest: dest.clone(),
1808 draft: draft.clone(),
1809 credential_fingerprint: fingerprint(cred),
1810 });
1811 Ok(Filed {
1812 url: format!("https://tracker.fake.test/browse/{id}"),
1813 external_id: id,
1814 })
1815 }
1816
1817 async fn status(
1818 &self,
1819 dest: &Destination,
1820 cred: &Credential,
1821 external_id: &str,
1822 ) -> Result<TicketStatus, TrackerError> {
1823 if let Some(error) = Self::error_for(&self.mode_for(dest)) {
1824 return Err(error);
1825 }
1826 self.inner
1827 .statused
1828 .lock()
1829 .expect("tracker lock")
1830 .push(StatusedCall {
1831 dest: dest.clone(),
1832 external_id: external_id.to_owned(),
1833 credential_fingerprint: fingerprint(cred),
1834 });
1835 Ok(TicketStatus {
1836 external_id: external_id.to_owned(),
1837 state: self.state(),
1838 url: None,
1839 })
1840 }
1841
1842 async fn comment(
1843 &self,
1844 dest: &Destination,
1845 cred: &Credential,
1846 external_id: &str,
1847 comment: &TicketComment,
1848 ) -> Result<(), TrackerError> {
1849 if let Some(error) = Self::error_for(&self.mode_for(dest)) {
1850 return Err(error);
1851 }
1852 self.inner
1853 .commented
1854 .lock()
1855 .expect("tracker lock")
1856 .push(CommentedCall {
1857 dest: dest.clone(),
1858 external_id: external_id.to_owned(),
1859 comment: comment.clone(),
1860 credential_fingerprint: fingerprint(cred),
1861 });
1862 Ok(())
1863 }
1864}
1865
1866type RealtimeMembers = std::collections::HashMap<String, Vec<String>>;
1871type RealtimeBroadcasts = Vec<(String, Vec<u8>)>;
1873
1874#[derive(Clone, Default)]
1880pub struct FakeRealtime {
1881 broadcasts: Arc<Mutex<RealtimeBroadcasts>>,
1882 members: Arc<Mutex<RealtimeMembers>>,
1883}
1884
1885impl FakeRealtime {
1886 #[must_use]
1887 pub fn new() -> Self {
1888 Self::default()
1889 }
1890
1891 pub fn set_members(&self, room_id: &str, ids: &[&str]) {
1893 self.members.lock().expect("realtime lock").insert(
1894 room_id.to_owned(),
1895 ids.iter().map(|id| (*id).to_owned()).collect(),
1896 );
1897 }
1898
1899 #[must_use]
1901 pub fn broadcasts(&self) -> Vec<(String, Vec<u8>)> {
1902 self.broadcasts.lock().expect("realtime lock").clone()
1903 }
1904}
1905
1906#[async_trait]
1907impl cratefield_core::Realtime for FakeRealtime {
1908 async fn broadcast(
1909 &self,
1910 room_id: &str,
1911 message: &[u8],
1912 ) -> Result<(), cratefield_core::RealtimeError> {
1913 self.broadcasts
1914 .lock()
1915 .expect("realtime lock")
1916 .push((room_id.to_owned(), message.to_vec()));
1917 Ok(())
1918 }
1919
1920 async fn members(
1921 &self,
1922 room_id: &str,
1923 ) -> Result<Vec<cratefield_core::Member>, cratefield_core::RealtimeError> {
1924 Ok(self
1925 .members
1926 .lock()
1927 .expect("realtime lock")
1928 .get(room_id)
1929 .map(|ids| ids.iter().map(cratefield_core::Member::new).collect())
1930 .unwrap_or_default())
1931 }
1932}
1933
1934#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1940pub enum AuthMode {
1941 Anonymous,
1944 TokenIsTheSubject,
1948 NotVerified,
1952 Unavailable,
1954}
1955
1956pub struct FakeAuth {
1959 mode: AuthMode,
1960 calls: std::sync::Mutex<Vec<String>>,
1961}
1962
1963impl FakeAuth {
1964 #[must_use]
1966 pub fn new(mode: AuthMode) -> Self {
1967 Self {
1968 mode,
1969 calls: std::sync::Mutex::new(Vec::new()),
1970 }
1971 }
1972
1973 #[must_use]
1975 pub fn subjects() -> Self {
1976 Self::new(AuthMode::TokenIsTheSubject)
1977 }
1978
1979 #[must_use]
1982 pub fn calls(&self) -> Vec<String> {
1983 self.calls.lock().expect("uncontended").clone()
1984 }
1985}
1986
1987#[async_trait::async_trait]
1988impl cratefield_core::Auth for FakeAuth {
1989 async fn identify(
1990 &self,
1991 headers: &http::HeaderMap,
1992 ) -> Result<cratefield_core::Caller, cratefield_core::AuthError> {
1993 let presented = headers
1994 .get(http::header::AUTHORIZATION)
1995 .and_then(|value| value.to_str().ok())
1996 .and_then(|value| value.strip_prefix("Bearer "))
1997 .map(str::to_owned);
1998 self.calls
1999 .lock()
2000 .expect("uncontended")
2001 .push(presented.clone().unwrap_or_default());
2002 match self.mode {
2003 AuthMode::Anonymous => Ok(cratefield_core::Caller::Anonymous),
2004 AuthMode::Unavailable => Err(cratefield_core::AuthError::Unavailable(
2005 "the fake is in Unavailable mode".to_owned(),
2006 )),
2007 AuthMode::NotVerified | AuthMode::TokenIsTheSubject => match presented {
2008 None => Ok(cratefield_core::Caller::Anonymous),
2009 Some(_) if self.mode == AuthMode::NotVerified => {
2010 Err(cratefield_core::AuthError::NotVerified)
2011 }
2012 Some(token) => Ok(cratefield_core::Caller::Subject(
2013 cratefield_core::Subject::new(token).session("fake-session"),
2014 )),
2015 },
2016 }
2017 }
2018}
2019
2020#[derive(Clone)]
2031pub struct FakeCustomHostnames {
2032 inner: Arc<FakeCustomHostnamesInner>,
2033}
2034
2035struct FakeCustomHostnamesInner {
2036 own_zone: String,
2037 hostnames: Mutex<BTreeMap<String, CustomHostname>>,
2038 calls: Mutex<Vec<String>>,
2039 refusals: Mutex<VecDeque<CustomHostnameError>>,
2041 next_id: AtomicUsize,
2042}
2043
2044impl FakeCustomHostnames {
2045 #[must_use]
2048 pub fn new(own_zone: impl Into<String>) -> Self {
2049 Self {
2050 inner: Arc::new(FakeCustomHostnamesInner {
2051 own_zone: own_zone.into(),
2052 hostnames: Mutex::new(BTreeMap::new()),
2053 calls: Mutex::new(Vec::new()),
2054 refusals: Mutex::new(VecDeque::new()),
2055 next_id: AtomicUsize::new(0),
2056 }),
2057 }
2058 }
2059
2060 #[must_use]
2062 pub fn calls(&self) -> Vec<String> {
2063 self.inner
2064 .calls
2065 .lock()
2066 .expect("custom hostnames lock")
2067 .clone()
2068 }
2069
2070 pub fn activate(&self, hostname: &str) {
2075 let key = self.key(hostname);
2076 if let Some(claim) = self
2077 .inner
2078 .hostnames
2079 .lock()
2080 .expect("custom hostnames lock")
2081 .get_mut(&key)
2082 {
2083 claim.status = ProviderStatus::Active;
2084 claim.certificate = CertificateStatus::Active;
2085 claim.validation.clear();
2086 }
2087 }
2088
2089 pub fn fail(&self, hostname: &str, reason: &str) {
2091 let key = self.key(hostname);
2092 if let Some(claim) = self
2093 .inner
2094 .hostnames
2095 .lock()
2096 .expect("custom hostnames lock")
2097 .get_mut(&key)
2098 {
2099 claim.status = ProviderStatus::Failed {
2100 reason: reason.to_owned(),
2101 };
2102 }
2103 }
2104
2105 pub fn refuse_next(&self, error: CustomHostnameError) {
2108 self.inner
2109 .refusals
2110 .lock()
2111 .expect("custom hostnames lock")
2112 .push_back(error);
2113 }
2114
2115 fn key(&self, hostname: &str) -> String {
2118 check_hostname(hostname, &self.inner.own_zone)
2119 .unwrap_or_else(|_| hostname.trim().trim_end_matches('.').to_ascii_lowercase())
2120 }
2121
2122 fn log(&self, call: String) {
2123 self.inner
2124 .calls
2125 .lock()
2126 .expect("custom hostnames lock")
2127 .push(call);
2128 }
2129
2130 fn take_refusal(&self) -> Option<CustomHostnameError> {
2131 self.inner
2132 .refusals
2133 .lock()
2134 .expect("custom hostnames lock")
2135 .pop_front()
2136 }
2137}
2138
2139#[async_trait]
2140impl CustomHostnames for FakeCustomHostnames {
2141 async fn create(&self, claim: &HostnameClaim) -> Result<CustomHostname, CustomHostnameError> {
2142 self.log(format!("create {}", claim.hostname));
2143 if let Some(error) = self.take_refusal() {
2144 return Err(error);
2145 }
2146 let hostname = check_hostname(&claim.hostname, &self.inner.own_zone)
2147 .map_err(CustomHostnameError::Refused)?;
2148 let mut hostnames = self.inner.hostnames.lock().expect("custom hostnames lock");
2149 if hostnames.contains_key(&hostname) {
2150 return Err(CustomHostnameError::AlreadyExists);
2151 }
2152 let id = format!(
2153 "fake-{}",
2154 self.inner.next_id.fetch_add(1, Ordering::Relaxed)
2155 );
2156 let created = CustomHostname {
2157 id: id.clone(),
2158 hostname: hostname.clone(),
2159 status: ProviderStatus::Pending,
2160 certificate: CertificateStatus::Pending,
2161 validation: vec![Validation {
2162 record_type: DnsRecordType::Txt,
2163 name: format!("_cf-custom-hostname.{hostname}"),
2164 value: id,
2165 }],
2166 };
2167 hostnames.insert(hostname, created.clone());
2168 Ok(created)
2169 }
2170
2171 async fn get(&self, hostname: &str) -> Result<Option<CustomHostname>, CustomHostnameError> {
2172 self.log(format!("get {hostname}"));
2173 if let Some(error) = self.take_refusal() {
2174 return Err(error);
2175 }
2176 let key = self.key(hostname);
2177 Ok(self
2178 .inner
2179 .hostnames
2180 .lock()
2181 .expect("custom hostnames lock")
2182 .get(&key)
2183 .cloned())
2184 }
2185
2186 async fn delete(&self, hostname: &str) -> Result<(), CustomHostnameError> {
2187 self.log(format!("delete {hostname}"));
2188 if let Some(error) = self.take_refusal() {
2189 return Err(error);
2190 }
2191 let key = self.key(hostname);
2192 self.inner
2193 .hostnames
2194 .lock()
2195 .expect("custom hostnames lock")
2196 .remove(&key);
2197 Ok(())
2198 }
2199
2200 async fn refresh(&self, hostname: &str) -> Result<CustomHostname, CustomHostnameError> {
2201 self.log(format!("refresh {hostname}"));
2202 if let Some(error) = self.take_refusal() {
2203 return Err(error);
2204 }
2205 let key = self.key(hostname);
2206 self.inner
2207 .hostnames
2208 .lock()
2209 .expect("custom hostnames lock")
2210 .get(&key)
2211 .cloned()
2212 .ok_or(CustomHostnameError::NotFound)
2213 }
2214}
2215
2216#[cfg(test)]
2217mod tests {
2218 use super::*;
2219 use cratefield_core::Blob as _;
2220
2221 fn json_body(response: &Response<Bytes>) -> serde_json::Value {
2222 serde_json::from_slice(response.body()).expect("the body is json")
2223 }
2224
2225 #[test]
2226 fn problem_json_carries_the_problem_fields_and_media_type() {
2227 let response = problem_json(
2228 StatusCode::TOO_MANY_REQUESTS,
2229 "Too Many Requests",
2230 "slow down",
2231 );
2232 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
2233 assert_eq!(response.headers()[CONTENT_TYPE], "application/problem+json");
2234 let body = json_body(&response);
2235 assert_eq!(body["type"], "about:blank");
2236 assert_eq!(body["title"], "Too Many Requests");
2237 assert_eq!(body["status"], 429);
2238 assert_eq!(body["detail"], "slow down");
2239 }
2240
2241 #[test]
2242 fn error_json_is_the_colonizer_shape() {
2243 let response = error_json(StatusCode::BAD_REQUEST, "bad request");
2244 assert_eq!(response.headers()[CONTENT_TYPE], "application/json");
2245 assert_eq!(json_body(&response)["error"], "bad request");
2246 }
2247
2248 #[test]
2249 fn with_retry_after_sets_the_header_in_seconds() {
2250 let response = with_retry_after(error_json(StatusCode::TOO_MANY_REQUESTS, "slow"), 42);
2251 assert_eq!(response.headers()[RETRY_AFTER], "42");
2252 }
2253
2254 #[test]
2255 fn a_preset_answers_through_the_port() {
2256 let http = FakeHttpClient::owlpost_rate_limited(7);
2259 let request = Request::builder()
2260 .uri("https://api.owlpost.fake/v1/emails")
2261 .body(Bytes::new())
2262 .expect("the request builds");
2263 let response = pollster::block_on(http.send(request)).expect("the preset answers");
2264 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
2265 assert_eq!(response.headers()[RETRY_AFTER], "7");
2266 assert_eq!(json_body(&response)["detail"], "slow down");
2267 }
2268
2269 #[pollster::test]
2270 async fn memory_blob_presigns_and_verifies_back() {
2271 let at = time::OffsetDateTime::from_unix_timestamp(1_577_836_800).expect("a valid epoch");
2274 let blob = MemoryBlob::with_presign(Arc::new(FixedClock(at)));
2275 let ttl = Duration::from_secs(3600);
2276
2277 let get = blob.signed_url("cms/clip.mp3", ttl).await.unwrap();
2278 assert!(get.starts_with("https://blob.test/cms/clip.mp3?"), "{get}");
2279 assert_eq!(
2280 blob.verify_presigned(&get, "GET", &[]).unwrap(),
2281 "cms/clip.mp3"
2282 );
2283
2284 let put = blob
2285 .signed_put_url("cms/clip.mp3", "audio/mpeg", Some(3), ttl)
2286 .await
2287 .unwrap();
2288 assert_eq!((put.method, put.headers.len()), ("PUT", 2));
2289 assert!(blob.verify_presigned(&put.url, "PUT", &put.headers).is_ok());
2290 assert!(blob.verify_presigned(&put.url, "PUT", &[]).is_err());
2292
2293 assert!(matches!(
2295 MemoryBlob::new().signed_url("k", ttl).await.unwrap_err(),
2296 cratefield_core::BlobError::Unsupported(_)
2297 ));
2298 }
2299}