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, Captcha, CaptchaError, CertificateStatus, Classifier,
19 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 match self.mode_for(prompt.tier) {
1136 TextModelMode::Reply(text) => {
1137 self.record(prompt);
1138 let (input_tokens, output_tokens) = fake_usage(prompt, &text);
1139 Ok(
1140 Completion::new(text, format!("fake-{}", prompt.tier.name()))
1141 .usage(input_tokens, output_tokens),
1142 )
1143 }
1144 TextModelMode::Complete(completion) => {
1145 self.record(prompt);
1146 Ok(completion)
1147 }
1148 TextModelMode::NotConfigured => Err(TextModelError::NotConfigured),
1149 TextModelMode::Rejected(message) => Err(TextModelError::Rejected(message)),
1150 TextModelMode::Transient { retry_after } => {
1151 Err(TextModelError::Transient { retry_after })
1152 }
1153 TextModelMode::Error(error) => Err(error),
1154 }
1155 }
1156}
1157
1158#[derive(Debug, Clone, PartialEq)]
1170pub enum ClassifierMode {
1171 Answers(BTreeMap<String, Answer>),
1175 Deterministic,
1180 NotConfigured,
1182 Rejected(String),
1184 Transient { retry_after: Option<Duration> },
1186 Error(ClassifierError),
1188}
1189
1190#[derive(Debug, Clone, PartialEq, Eq)]
1194pub struct Ask {
1195 pub state: String,
1197 pub question_ids: Vec<String>,
1199}
1200
1201#[derive(Clone)]
1215pub struct FakeClassifier {
1216 inner: Arc<FakeClassifierInner>,
1217}
1218
1219struct FakeClassifierInner {
1220 mode: Mutex<ClassifierMode>,
1221 per_question: Mutex<BTreeMap<String, Answer>>,
1222 asks: Mutex<Vec<Ask>>,
1223 profile: Mutex<ClassifierProfile>,
1224}
1225
1226fn deterministic_answer(question: &Question) -> Answer {
1232 let labels = question.labels();
1233 let chosen = labels.first().copied().unwrap_or_default();
1234 let share = if labels.len() > 1 {
1237 0.4 / f32::from(u16::try_from(labels.len() - 1).unwrap_or(1))
1238 } else {
1239 0.0
1240 };
1241 let probabilities: BTreeMap<String, f32> = labels
1242 .iter()
1243 .map(|label| {
1244 let mass = if *label == chosen { 0.6 } else { share };
1245 ((*label).to_owned(), mass)
1246 })
1247 .collect();
1248 match question {
1249 Question::Choice { .. } => Answer::choice(chosen, probabilities),
1250 Question::Noul { .. } => Answer::noul(chosen == "true", probabilities),
1251 Question::Score { .. } => Answer::new(
1255 AnswerValue::Score(chosen.parse().unwrap_or_default()),
1256 probabilities,
1257 0.6,
1258 ),
1259 }
1260}
1261
1262impl FakeClassifier {
1263 #[must_use]
1264 pub fn new(mode: ClassifierMode) -> Self {
1265 Self {
1266 inner: Arc::new(FakeClassifierInner {
1267 mode: Mutex::new(mode),
1268 per_question: Mutex::new(BTreeMap::new()),
1269 asks: Mutex::new(Vec::new()),
1270 profile: Mutex::new(ClassifierProfile::new(
1271 Calibration::Classifier,
1272 cratefield_core::DEFAULT_MAX_STATE_CHARS,
1273 )),
1274 }),
1275 }
1276 }
1277
1278 #[must_use]
1281 pub fn asks(&self) -> Vec<Ask> {
1282 self.inner.asks.lock().expect("classifier lock").clone()
1283 }
1284
1285 #[must_use]
1287 pub fn last(&self) -> Option<Ask> {
1288 self.inner
1289 .asks
1290 .lock()
1291 .expect("classifier lock")
1292 .last()
1293 .cloned()
1294 }
1295
1296 pub fn set_mode(&self, mode: ClassifierMode) {
1299 *self.inner.mode.lock().expect("classifier lock") = mode;
1300 }
1301
1302 pub fn set_answer_for(&self, question_id: impl Into<String>, answer: Answer) {
1306 self.inner
1307 .per_question
1308 .lock()
1309 .expect("classifier lock")
1310 .insert(question_id.into(), answer);
1311 }
1312
1313 pub fn clear_answer_for(&self, question_id: &str) {
1315 self.inner
1316 .per_question
1317 .lock()
1318 .expect("classifier lock")
1319 .remove(question_id);
1320 }
1321
1322 pub fn set_profile(&self, calibration: Calibration, max_state_chars: usize) {
1326 *self.inner.profile.lock().expect("classifier lock") =
1327 ClassifierProfile::new(calibration, max_state_chars);
1328 }
1329}
1330
1331impl Default for FakeClassifier {
1332 fn default() -> Self {
1333 Self::new(ClassifierMode::Deterministic)
1334 }
1335}
1336
1337#[async_trait]
1338impl Classifier for FakeClassifier {
1339 fn profile(&self) -> ClassifierProfile {
1340 *self.inner.profile.lock().expect("classifier lock")
1341 }
1342
1343 async fn ask(
1344 &self,
1345 state: &str,
1346 questions: &BTreeMap<String, Question>,
1347 ) -> Result<BTreeMap<String, Answer>, ClassifierError> {
1348 validate_questions(questions)?;
1351
1352 let mode = self.inner.mode.lock().expect("classifier lock").clone();
1353 match mode {
1354 ClassifierMode::NotConfigured => return Err(ClassifierError::NotConfigured),
1355 ClassifierMode::Rejected(message) => return Err(ClassifierError::Rejected(message)),
1356 ClassifierMode::Transient { retry_after } => {
1357 return Err(ClassifierError::Transient { retry_after });
1358 }
1359 ClassifierMode::Error(error) => return Err(error),
1360 ClassifierMode::Answers(_) | ClassifierMode::Deterministic => {}
1361 }
1362
1363 let overrides = self.inner.per_question.lock().expect("classifier lock");
1364 let answers: BTreeMap<String, Answer> = questions
1365 .iter()
1366 .map(|(id, question)| {
1367 let answer = overrides.get(id).cloned().unwrap_or_else(|| match &mode {
1368 ClassifierMode::Answers(map) => map
1369 .get(id)
1370 .cloned()
1371 .unwrap_or_else(|| deterministic_answer(question)),
1372 _ => deterministic_answer(question),
1373 });
1374 (id.clone(), answer)
1375 })
1376 .collect();
1377 drop(overrides);
1378
1379 self.inner.asks.lock().expect("classifier lock").push(Ask {
1380 state: state.to_owned(),
1381 question_ids: questions.keys().cloned().collect(),
1382 });
1383 Ok(answers)
1384 }
1385}
1386
1387#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1392pub enum PaymentsMode {
1393 Ok,
1395 NotConfigured,
1397 Transient,
1399}
1400
1401#[derive(Debug, Clone, PartialEq, Eq)]
1403pub enum PaymentsCall {
1404 Checkout,
1405 SubscriptionCheckout,
1406 ConnectAccountLink,
1407 ChargeWithTransfer,
1408 Refund,
1409 VerifyWebhook,
1410}
1411
1412#[derive(Clone)]
1416pub struct FakePayments {
1417 inner: Arc<FakePaymentsInner>,
1418}
1419
1420struct FakePaymentsInner {
1421 mode: Mutex<PaymentsMode>,
1422 calls: Mutex<Vec<PaymentsCall>>,
1423}
1424
1425impl FakePayments {
1426 #[must_use]
1427 pub fn new(mode: PaymentsMode) -> Self {
1428 Self {
1429 inner: Arc::new(FakePaymentsInner {
1430 mode: Mutex::new(mode),
1431 calls: Mutex::new(Vec::new()),
1432 }),
1433 }
1434 }
1435
1436 #[must_use]
1438 pub fn calls(&self) -> Vec<PaymentsCall> {
1439 self.inner.calls.lock().expect("payments lock").clone()
1440 }
1441
1442 pub fn set_mode(&self, mode: PaymentsMode) {
1443 *self.inner.mode.lock().expect("payments lock") = mode;
1444 }
1445
1446 fn record(&self, call: PaymentsCall) {
1447 self.inner.calls.lock().expect("payments lock").push(call);
1448 }
1449
1450 fn guard(&self) -> Result<(), cratefield_core::PaymentsError> {
1451 match *self.inner.mode.lock().expect("payments lock") {
1452 PaymentsMode::Ok => Ok(()),
1453 PaymentsMode::NotConfigured => Err(cratefield_core::PaymentsError::NotConfigured),
1454 PaymentsMode::Transient => Err(cratefield_core::PaymentsError::Transient(
1455 "fake payments failure".to_owned(),
1456 )),
1457 }
1458 }
1459}
1460
1461impl Default for FakePayments {
1462 fn default() -> Self {
1463 Self::new(PaymentsMode::Ok)
1464 }
1465}
1466
1467#[async_trait]
1468impl cratefield_core::Payments for FakePayments {
1469 async fn create_checkout(
1470 &self,
1471 _request: &cratefield_core::CheckoutRequest,
1472 ) -> Result<cratefield_core::CheckoutSession, cratefield_core::PaymentsError> {
1473 self.guard()?;
1474 self.record(PaymentsCall::Checkout);
1475 Ok(cratefield_core::CheckoutSession {
1476 id: "cs_fake".to_owned(),
1477 url: "https://checkout.stripe.test/cs_fake".to_owned(),
1478 })
1479 }
1480
1481 async fn create_subscription_checkout(
1482 &self,
1483 _request: &cratefield_core::SubscriptionCheckoutRequest,
1484 ) -> Result<cratefield_core::CheckoutSession, cratefield_core::PaymentsError> {
1485 self.guard()?;
1486 self.record(PaymentsCall::SubscriptionCheckout);
1487 Ok(cratefield_core::CheckoutSession {
1488 id: "cs_sub_fake".to_owned(),
1489 url: "https://checkout.stripe.test/cs_sub_fake".to_owned(),
1490 })
1491 }
1492
1493 async fn create_connect_account_link(
1494 &self,
1495 _request: &cratefield_core::ConnectAccountLinkRequest,
1496 ) -> Result<cratefield_core::ConnectAccountLink, cratefield_core::PaymentsError> {
1497 self.guard()?;
1498 self.record(PaymentsCall::ConnectAccountLink);
1499 Ok(cratefield_core::ConnectAccountLink {
1500 account_id: "acct_fake".to_owned(),
1501 url: "https://connect.stripe.test/acct_fake".to_owned(),
1502 })
1503 }
1504
1505 async fn charge_with_transfer(
1506 &self,
1507 _request: &cratefield_core::TransferCharge,
1508 ) -> Result<cratefield_core::Charge, cratefield_core::PaymentsError> {
1509 self.guard()?;
1510 self.record(PaymentsCall::ChargeWithTransfer);
1511 Ok(cratefield_core::Charge {
1512 id: "pi_fake".to_owned(),
1513 status: "succeeded".to_owned(),
1514 })
1515 }
1516
1517 async fn refund(
1518 &self,
1519 _request: &cratefield_core::RefundRequest,
1520 ) -> Result<cratefield_core::Refund, cratefield_core::PaymentsError> {
1521 self.guard()?;
1522 self.record(PaymentsCall::Refund);
1523 Ok(cratefield_core::Refund {
1524 id: "re_fake".to_owned(),
1525 })
1526 }
1527
1528 async fn verify_webhook(
1529 &self,
1530 signature_header: &str,
1531 _body: &[u8],
1532 ) -> Result<cratefield_core::WebhookEvent, cratefield_core::PaymentsError> {
1533 self.record(PaymentsCall::VerifyWebhook);
1534 if signature_header == "invalid" {
1535 return Err(cratefield_core::PaymentsError::SignatureInvalid(
1536 "fake tampered signature".to_owned(),
1537 ));
1538 }
1539 self.guard()?;
1540 Ok(cratefield_core::WebhookEvent {
1541 id: "evt_fake".to_owned(),
1542 kind: "checkout.session.completed".to_owned(),
1543 data: serde_json::json!({ "object": "checkout.session" }),
1544 })
1545 }
1546}
1547
1548fn fingerprint(credential: &Credential) -> String {
1564 use std::hash::{Hash, Hasher};
1565
1566 let mut hasher = std::hash::DefaultHasher::new();
1569 credential.expose().hash(&mut hasher);
1570 format!("fp:{:016x}", hasher.finish())
1571}
1572
1573#[derive(Debug, Clone, PartialEq, Eq)]
1582pub enum TrackerMode {
1583 FileOk,
1585 NotConfigured,
1587 Unauthorized,
1589 Rejected,
1592 Transient,
1594 Error(TrackerError),
1596}
1597
1598#[derive(Debug, Clone, PartialEq, Eq)]
1604pub struct FiledCall {
1605 pub dest: Destination,
1607 pub draft: TicketDraft,
1609 pub credential_fingerprint: String,
1611}
1612
1613#[derive(Debug, Clone, PartialEq, Eq)]
1615pub struct StatusedCall {
1616 pub dest: Destination,
1618 pub external_id: String,
1620 pub credential_fingerprint: String,
1622}
1623
1624#[derive(Debug, Clone, PartialEq, Eq)]
1626pub struct CommentedCall {
1627 pub dest: Destination,
1629 pub external_id: String,
1631 pub comment: TicketComment,
1633 pub credential_fingerprint: String,
1635}
1636
1637#[derive(Clone, Debug)]
1650pub struct FakeTracker {
1651 inner: Arc<FakeTrackerInner>,
1652}
1653
1654#[derive(Debug)]
1655struct FakeTrackerInner {
1656 mode: Mutex<TrackerMode>,
1657 per_destination: Mutex<HashMap<Destination, TrackerMode>>,
1658 filed: Mutex<Vec<FiledCall>>,
1659 statused: Mutex<Vec<StatusedCall>>,
1660 commented: Mutex<Vec<CommentedCall>>,
1661 state: Mutex<TicketState>,
1662}
1663
1664impl FakeTracker {
1665 #[must_use]
1666 pub fn new(mode: TrackerMode) -> Self {
1667 Self {
1668 inner: Arc::new(FakeTrackerInner {
1669 mode: Mutex::new(mode),
1670 per_destination: Mutex::new(HashMap::new()),
1671 filed: Mutex::new(Vec::new()),
1672 statused: Mutex::new(Vec::new()),
1673 commented: Mutex::new(Vec::new()),
1674 state: Mutex::new(TicketState::Open),
1675 }),
1676 }
1677 }
1678
1679 #[must_use]
1681 pub fn filed(&self) -> Vec<FiledCall> {
1682 self.inner.filed.lock().expect("tracker lock").clone()
1683 }
1684
1685 #[must_use]
1687 pub fn last_filed(&self) -> Option<FiledCall> {
1688 self.inner
1689 .filed
1690 .lock()
1691 .expect("tracker lock")
1692 .last()
1693 .cloned()
1694 }
1695
1696 #[must_use]
1698 pub fn statused(&self) -> Vec<StatusedCall> {
1699 self.inner.statused.lock().expect("tracker lock").clone()
1700 }
1701
1702 #[must_use]
1704 pub fn commented(&self) -> Vec<CommentedCall> {
1705 self.inner.commented.lock().expect("tracker lock").clone()
1706 }
1707
1708 pub fn set_mode(&self, mode: TrackerMode) {
1711 *self.inner.mode.lock().expect("tracker lock") = mode;
1712 }
1713
1714 pub fn set_mode_for(&self, dest: &Destination, mode: TrackerMode) {
1717 self.inner
1718 .per_destination
1719 .lock()
1720 .expect("tracker lock")
1721 .insert(dest.clone(), mode);
1722 }
1723
1724 pub fn clear_mode_for(&self, dest: &Destination) {
1727 self.inner
1728 .per_destination
1729 .lock()
1730 .expect("tracker lock")
1731 .remove(dest);
1732 }
1733
1734 #[must_use]
1736 pub fn mode_for(&self, dest: &Destination) -> TrackerMode {
1737 self.inner
1738 .per_destination
1739 .lock()
1740 .expect("tracker lock")
1741 .get(dest)
1742 .cloned()
1743 .unwrap_or_else(|| self.inner.mode.lock().expect("tracker lock").clone())
1744 }
1745
1746 pub fn set_state(&self, state: TicketState) {
1748 *self.inner.state.lock().expect("tracker lock") = state;
1749 }
1750
1751 #[must_use]
1753 pub fn state(&self) -> TicketState {
1754 *self.inner.state.lock().expect("tracker lock")
1755 }
1756
1757 fn error_for(mode: &TrackerMode) -> Option<TrackerError> {
1761 match mode {
1762 TrackerMode::FileOk => None,
1763 TrackerMode::NotConfigured => Some(TrackerError::NotConfigured),
1764 TrackerMode::Unauthorized => Some(TrackerError::Unauthorized),
1765 TrackerMode::Rejected => {
1766 Some(TrackerError::Rejected("fake tracker rejection".to_owned()))
1767 }
1768 TrackerMode::Transient => Some(TrackerError::Transient { retry_after: None }),
1769 TrackerMode::Error(error) => Some(error.clone()),
1770 }
1771 }
1772}
1773
1774impl Default for FakeTracker {
1775 fn default() -> Self {
1776 Self::new(TrackerMode::FileOk)
1777 }
1778}
1779
1780#[async_trait]
1781impl Tracker for FakeTracker {
1782 async fn file(
1783 &self,
1784 dest: &Destination,
1785 cred: &Credential,
1786 draft: &TicketDraft,
1787 ) -> Result<Filed, TrackerError> {
1788 if let Some(error) = Self::error_for(&self.mode_for(dest)) {
1789 return Err(error);
1790 }
1791 let id = format!(
1792 "fake-{}",
1793 self.inner.filed.lock().expect("tracker lock").len()
1794 );
1795 self.inner
1796 .filed
1797 .lock()
1798 .expect("tracker lock")
1799 .push(FiledCall {
1800 dest: dest.clone(),
1801 draft: draft.clone(),
1802 credential_fingerprint: fingerprint(cred),
1803 });
1804 Ok(Filed {
1805 url: format!("https://tracker.fake.test/browse/{id}"),
1806 external_id: id,
1807 })
1808 }
1809
1810 async fn status(
1811 &self,
1812 dest: &Destination,
1813 cred: &Credential,
1814 external_id: &str,
1815 ) -> Result<TicketStatus, TrackerError> {
1816 if let Some(error) = Self::error_for(&self.mode_for(dest)) {
1817 return Err(error);
1818 }
1819 self.inner
1820 .statused
1821 .lock()
1822 .expect("tracker lock")
1823 .push(StatusedCall {
1824 dest: dest.clone(),
1825 external_id: external_id.to_owned(),
1826 credential_fingerprint: fingerprint(cred),
1827 });
1828 Ok(TicketStatus {
1829 external_id: external_id.to_owned(),
1830 state: self.state(),
1831 url: None,
1832 })
1833 }
1834
1835 async fn comment(
1836 &self,
1837 dest: &Destination,
1838 cred: &Credential,
1839 external_id: &str,
1840 comment: &TicketComment,
1841 ) -> Result<(), TrackerError> {
1842 if let Some(error) = Self::error_for(&self.mode_for(dest)) {
1843 return Err(error);
1844 }
1845 self.inner
1846 .commented
1847 .lock()
1848 .expect("tracker lock")
1849 .push(CommentedCall {
1850 dest: dest.clone(),
1851 external_id: external_id.to_owned(),
1852 comment: comment.clone(),
1853 credential_fingerprint: fingerprint(cred),
1854 });
1855 Ok(())
1856 }
1857}
1858
1859type RealtimeMembers = std::collections::HashMap<String, Vec<String>>;
1864type RealtimeBroadcasts = Vec<(String, Vec<u8>)>;
1866
1867#[derive(Clone, Default)]
1873pub struct FakeRealtime {
1874 broadcasts: Arc<Mutex<RealtimeBroadcasts>>,
1875 members: Arc<Mutex<RealtimeMembers>>,
1876}
1877
1878impl FakeRealtime {
1879 #[must_use]
1880 pub fn new() -> Self {
1881 Self::default()
1882 }
1883
1884 pub fn set_members(&self, room_id: &str, ids: &[&str]) {
1886 self.members.lock().expect("realtime lock").insert(
1887 room_id.to_owned(),
1888 ids.iter().map(|id| (*id).to_owned()).collect(),
1889 );
1890 }
1891
1892 #[must_use]
1894 pub fn broadcasts(&self) -> Vec<(String, Vec<u8>)> {
1895 self.broadcasts.lock().expect("realtime lock").clone()
1896 }
1897}
1898
1899#[async_trait]
1900impl cratefield_core::Realtime for FakeRealtime {
1901 async fn broadcast(
1902 &self,
1903 room_id: &str,
1904 message: &[u8],
1905 ) -> Result<(), cratefield_core::RealtimeError> {
1906 self.broadcasts
1907 .lock()
1908 .expect("realtime lock")
1909 .push((room_id.to_owned(), message.to_vec()));
1910 Ok(())
1911 }
1912
1913 async fn members(
1914 &self,
1915 room_id: &str,
1916 ) -> Result<Vec<cratefield_core::Member>, cratefield_core::RealtimeError> {
1917 Ok(self
1918 .members
1919 .lock()
1920 .expect("realtime lock")
1921 .get(room_id)
1922 .map(|ids| ids.iter().map(cratefield_core::Member::new).collect())
1923 .unwrap_or_default())
1924 }
1925}
1926
1927#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1933pub enum AuthMode {
1934 Anonymous,
1937 TokenIsTheSubject,
1941 NotVerified,
1945 Unavailable,
1947}
1948
1949pub struct FakeAuth {
1952 mode: AuthMode,
1953 calls: std::sync::Mutex<Vec<String>>,
1954}
1955
1956impl FakeAuth {
1957 #[must_use]
1959 pub fn new(mode: AuthMode) -> Self {
1960 Self {
1961 mode,
1962 calls: std::sync::Mutex::new(Vec::new()),
1963 }
1964 }
1965
1966 #[must_use]
1968 pub fn subjects() -> Self {
1969 Self::new(AuthMode::TokenIsTheSubject)
1970 }
1971
1972 #[must_use]
1975 pub fn calls(&self) -> Vec<String> {
1976 self.calls.lock().expect("uncontended").clone()
1977 }
1978}
1979
1980#[async_trait::async_trait]
1981impl cratefield_core::Auth for FakeAuth {
1982 async fn identify(
1983 &self,
1984 headers: &http::HeaderMap,
1985 ) -> Result<cratefield_core::Caller, cratefield_core::AuthError> {
1986 let presented = headers
1987 .get(http::header::AUTHORIZATION)
1988 .and_then(|value| value.to_str().ok())
1989 .and_then(|value| value.strip_prefix("Bearer "))
1990 .map(str::to_owned);
1991 self.calls
1992 .lock()
1993 .expect("uncontended")
1994 .push(presented.clone().unwrap_or_default());
1995 match self.mode {
1996 AuthMode::Anonymous => Ok(cratefield_core::Caller::Anonymous),
1997 AuthMode::Unavailable => Err(cratefield_core::AuthError::Unavailable(
1998 "the fake is in Unavailable mode".to_owned(),
1999 )),
2000 AuthMode::NotVerified | AuthMode::TokenIsTheSubject => match presented {
2001 None => Ok(cratefield_core::Caller::Anonymous),
2002 Some(_) if self.mode == AuthMode::NotVerified => {
2003 Err(cratefield_core::AuthError::NotVerified)
2004 }
2005 Some(token) => Ok(cratefield_core::Caller::Subject(
2006 cratefield_core::Subject::new(token).session("fake-session"),
2007 )),
2008 },
2009 }
2010 }
2011}
2012
2013#[derive(Clone)]
2024pub struct FakeCustomHostnames {
2025 inner: Arc<FakeCustomHostnamesInner>,
2026}
2027
2028struct FakeCustomHostnamesInner {
2029 own_zone: String,
2030 hostnames: Mutex<BTreeMap<String, CustomHostname>>,
2031 calls: Mutex<Vec<String>>,
2032 refusals: Mutex<VecDeque<CustomHostnameError>>,
2034 next_id: AtomicUsize,
2035}
2036
2037impl FakeCustomHostnames {
2038 #[must_use]
2041 pub fn new(own_zone: impl Into<String>) -> Self {
2042 Self {
2043 inner: Arc::new(FakeCustomHostnamesInner {
2044 own_zone: own_zone.into(),
2045 hostnames: Mutex::new(BTreeMap::new()),
2046 calls: Mutex::new(Vec::new()),
2047 refusals: Mutex::new(VecDeque::new()),
2048 next_id: AtomicUsize::new(0),
2049 }),
2050 }
2051 }
2052
2053 #[must_use]
2055 pub fn calls(&self) -> Vec<String> {
2056 self.inner
2057 .calls
2058 .lock()
2059 .expect("custom hostnames lock")
2060 .clone()
2061 }
2062
2063 pub fn activate(&self, hostname: &str) {
2068 let key = self.key(hostname);
2069 if let Some(claim) = self
2070 .inner
2071 .hostnames
2072 .lock()
2073 .expect("custom hostnames lock")
2074 .get_mut(&key)
2075 {
2076 claim.status = ProviderStatus::Active;
2077 claim.certificate = CertificateStatus::Active;
2078 claim.validation.clear();
2079 }
2080 }
2081
2082 pub fn fail(&self, hostname: &str, reason: &str) {
2084 let key = self.key(hostname);
2085 if let Some(claim) = self
2086 .inner
2087 .hostnames
2088 .lock()
2089 .expect("custom hostnames lock")
2090 .get_mut(&key)
2091 {
2092 claim.status = ProviderStatus::Failed {
2093 reason: reason.to_owned(),
2094 };
2095 }
2096 }
2097
2098 pub fn refuse_next(&self, error: CustomHostnameError) {
2101 self.inner
2102 .refusals
2103 .lock()
2104 .expect("custom hostnames lock")
2105 .push_back(error);
2106 }
2107
2108 fn key(&self, hostname: &str) -> String {
2111 check_hostname(hostname, &self.inner.own_zone)
2112 .unwrap_or_else(|_| hostname.trim().trim_end_matches('.').to_ascii_lowercase())
2113 }
2114
2115 fn log(&self, call: String) {
2116 self.inner
2117 .calls
2118 .lock()
2119 .expect("custom hostnames lock")
2120 .push(call);
2121 }
2122
2123 fn take_refusal(&self) -> Option<CustomHostnameError> {
2124 self.inner
2125 .refusals
2126 .lock()
2127 .expect("custom hostnames lock")
2128 .pop_front()
2129 }
2130}
2131
2132#[async_trait]
2133impl CustomHostnames for FakeCustomHostnames {
2134 async fn create(&self, claim: &HostnameClaim) -> Result<CustomHostname, CustomHostnameError> {
2135 self.log(format!("create {}", claim.hostname));
2136 if let Some(error) = self.take_refusal() {
2137 return Err(error);
2138 }
2139 let hostname = check_hostname(&claim.hostname, &self.inner.own_zone)
2140 .map_err(CustomHostnameError::Refused)?;
2141 let mut hostnames = self.inner.hostnames.lock().expect("custom hostnames lock");
2142 if hostnames.contains_key(&hostname) {
2143 return Err(CustomHostnameError::AlreadyExists);
2144 }
2145 let id = format!(
2146 "fake-{}",
2147 self.inner.next_id.fetch_add(1, Ordering::Relaxed)
2148 );
2149 let created = CustomHostname {
2150 id: id.clone(),
2151 hostname: hostname.clone(),
2152 status: ProviderStatus::Pending,
2153 certificate: CertificateStatus::Pending,
2154 validation: vec![Validation {
2155 record_type: DnsRecordType::Txt,
2156 name: format!("_cf-custom-hostname.{hostname}"),
2157 value: id,
2158 }],
2159 };
2160 hostnames.insert(hostname, created.clone());
2161 Ok(created)
2162 }
2163
2164 async fn get(&self, hostname: &str) -> Result<Option<CustomHostname>, CustomHostnameError> {
2165 self.log(format!("get {hostname}"));
2166 if let Some(error) = self.take_refusal() {
2167 return Err(error);
2168 }
2169 let key = self.key(hostname);
2170 Ok(self
2171 .inner
2172 .hostnames
2173 .lock()
2174 .expect("custom hostnames lock")
2175 .get(&key)
2176 .cloned())
2177 }
2178
2179 async fn delete(&self, hostname: &str) -> Result<(), CustomHostnameError> {
2180 self.log(format!("delete {hostname}"));
2181 if let Some(error) = self.take_refusal() {
2182 return Err(error);
2183 }
2184 let key = self.key(hostname);
2185 self.inner
2186 .hostnames
2187 .lock()
2188 .expect("custom hostnames lock")
2189 .remove(&key);
2190 Ok(())
2191 }
2192
2193 async fn refresh(&self, hostname: &str) -> Result<CustomHostname, CustomHostnameError> {
2194 self.log(format!("refresh {hostname}"));
2195 if let Some(error) = self.take_refusal() {
2196 return Err(error);
2197 }
2198 let key = self.key(hostname);
2199 self.inner
2200 .hostnames
2201 .lock()
2202 .expect("custom hostnames lock")
2203 .get(&key)
2204 .cloned()
2205 .ok_or(CustomHostnameError::NotFound)
2206 }
2207}
2208
2209#[cfg(test)]
2210mod tests {
2211 use super::*;
2212 use cratefield_core::Blob as _;
2213
2214 fn json_body(response: &Response<Bytes>) -> serde_json::Value {
2215 serde_json::from_slice(response.body()).expect("the body is json")
2216 }
2217
2218 #[test]
2219 fn problem_json_carries_the_problem_fields_and_media_type() {
2220 let response = problem_json(
2221 StatusCode::TOO_MANY_REQUESTS,
2222 "Too Many Requests",
2223 "slow down",
2224 );
2225 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
2226 assert_eq!(response.headers()[CONTENT_TYPE], "application/problem+json");
2227 let body = json_body(&response);
2228 assert_eq!(body["type"], "about:blank");
2229 assert_eq!(body["title"], "Too Many Requests");
2230 assert_eq!(body["status"], 429);
2231 assert_eq!(body["detail"], "slow down");
2232 }
2233
2234 #[test]
2235 fn error_json_is_the_colonizer_shape() {
2236 let response = error_json(StatusCode::BAD_REQUEST, "bad request");
2237 assert_eq!(response.headers()[CONTENT_TYPE], "application/json");
2238 assert_eq!(json_body(&response)["error"], "bad request");
2239 }
2240
2241 #[test]
2242 fn with_retry_after_sets_the_header_in_seconds() {
2243 let response = with_retry_after(error_json(StatusCode::TOO_MANY_REQUESTS, "slow"), 42);
2244 assert_eq!(response.headers()[RETRY_AFTER], "42");
2245 }
2246
2247 #[test]
2248 fn a_preset_answers_through_the_port() {
2249 let http = FakeHttpClient::owlpost_rate_limited(7);
2252 let request = Request::builder()
2253 .uri("https://api.owlpost.fake/v1/emails")
2254 .body(Bytes::new())
2255 .expect("the request builds");
2256 let response = pollster::block_on(http.send(request)).expect("the preset answers");
2257 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
2258 assert_eq!(response.headers()[RETRY_AFTER], "7");
2259 assert_eq!(json_body(&response)["detail"], "slow down");
2260 }
2261
2262 #[pollster::test]
2263 async fn memory_blob_presigns_and_verifies_back() {
2264 let at = time::OffsetDateTime::from_unix_timestamp(1_577_836_800).expect("a valid epoch");
2267 let blob = MemoryBlob::with_presign(Arc::new(FixedClock(at)));
2268 let ttl = Duration::from_secs(3600);
2269
2270 let get = blob.signed_url("cms/clip.mp3", ttl).await.unwrap();
2271 assert!(get.starts_with("https://blob.test/cms/clip.mp3?"), "{get}");
2272 assert_eq!(
2273 blob.verify_presigned(&get, "GET", &[]).unwrap(),
2274 "cms/clip.mp3"
2275 );
2276
2277 let put = blob
2278 .signed_put_url("cms/clip.mp3", "audio/mpeg", Some(3), ttl)
2279 .await
2280 .unwrap();
2281 assert_eq!((put.method, put.headers.len()), ("PUT", 2));
2282 assert!(blob.verify_presigned(&put.url, "PUT", &put.headers).is_ok());
2283 assert!(blob.verify_presigned(&put.url, "PUT", &[]).is_err());
2285
2286 assert!(matches!(
2288 MemoryBlob::new().signed_url("k", ttl).await.unwrap_err(),
2289 cratefield_core::BlobError::Unsupported(_)
2290 ));
2291 }
2292}