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 ActorError, ActorHandlers, ActorStore, ActorWrites, Actors, Answer, AnswerValue, Calibration,
19 Capability, Captcha, CaptchaError, CertificateStatus, Classifier, ClassifierError,
20 ClassifierProfile, Clock, Completion, Credential, CustomHostname, CustomHostnameError,
21 CustomHostnames, Database, DbError, Decision, Defer, Destination, DnsRecordType, Filed,
22 HostnameClaim, HttpClient, HttpError, KeyValue, KvError, MailError, Mailer, Message, ModelTier,
23 Prompt, ProviderStatus, Question, RateLimitError, RateLimiter, Row, Rows, SendOutcome,
24 Statement, TextModel, TextModelError, TicketComment, TicketDraft, TicketState, TicketStatus,
25 Tracker, TrackerError, UrlSafety, UrlSafetyError, UrlSafetyLimits, UrlSafetySource, UrlVerdict,
26 Validation, Verdict, check_hostname, check_url, run_actor_alarm, run_actor_message,
27 validate_questions,
28};
29use futures_core::Stream;
30use futures_core::future::BoxFuture;
31use http::header::{CONTENT_TYPE, RETRY_AFTER};
32use http::{HeaderValue, Request, Response, StatusCode};
33use std::collections::{BTreeMap, HashMap, VecDeque};
34use std::pin::Pin;
35use std::sync::Arc;
36use std::sync::atomic::{AtomicUsize, Ordering};
37use std::task::{Context, Poll};
38use std::time::Duration;
39
40#[allow(clippy::disallowed_types)]
42use std::sync::Mutex;
43
44#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum MailerMode {
66 SendOk,
68 NotConfigured,
70 Fail,
72 Error(MailError),
74}
75
76#[derive(Clone)]
77pub struct FakeMailer {
78 inner: Arc<FakeMailerInner>,
79}
80
81struct FakeMailerInner {
82 mode: Mutex<MailerMode>,
83 sent: Mutex<Vec<Message>>,
84}
85
86impl FakeMailer {
87 #[must_use]
88 pub fn new(mode: MailerMode) -> Self {
89 Self {
90 inner: Arc::new(FakeMailerInner {
91 mode: Mutex::new(mode),
92 sent: Mutex::new(Vec::new()),
93 }),
94 }
95 }
96
97 #[must_use]
99 pub fn sent(&self) -> Vec<Message> {
100 self.inner.sent.lock().expect("mailer lock").clone()
101 }
102
103 #[must_use]
105 pub fn last_message(&self) -> Option<Message> {
106 self.inner.sent.lock().expect("mailer lock").last().cloned()
107 }
108
109 pub fn set_mode(&self, mode: MailerMode) {
111 *self.inner.mode.lock().expect("mailer lock") = mode;
112 }
113}
114
115#[async_trait]
116impl Mailer for FakeMailer {
117 async fn send(&self, message: Message) -> Result<SendOutcome, MailError> {
118 let mode = self.inner.mode.lock().expect("mailer lock").clone();
119 match mode {
120 MailerMode::SendOk => {
121 let id = format!(
122 "fake-{}",
123 self.inner.sent.lock().expect("mailer lock").len()
124 );
125 self.inner.sent.lock().expect("mailer lock").push(message);
126 Ok(SendOutcome::Sent { id })
127 }
128 MailerMode::NotConfigured => Ok(SendOutcome::NotConfigured),
129 MailerMode::Fail => Err(MailError::Upstream("fake mailer failure".to_string())),
130 MailerMode::Error(error) => Err(error),
131 }
132 }
133}
134
135#[derive(Clone)]
139pub struct FakeCaptcha {
140 allow_all: bool,
141 allowed_tokens: Arc<Vec<String>>,
142}
143
144impl FakeCaptcha {
145 #[must_use]
147 pub fn allow_all() -> Self {
148 Self {
149 allow_all: true,
150 allowed_tokens: Arc::new(Vec::new()),
151 }
152 }
153
154 #[must_use]
156 pub fn with_tokens(tokens: impl IntoIterator<Item = impl Into<String>>) -> Self {
157 Self {
158 allow_all: false,
159 allowed_tokens: Arc::new(tokens.into_iter().map(Into::into).collect()),
160 }
161 }
162}
163
164#[async_trait]
165impl Captcha for FakeCaptcha {
166 async fn verify(&self, token: &str, _remote_ip: Option<&str>) -> Result<Verdict, CaptchaError> {
167 let ok = self.allow_all || self.allowed_tokens.iter().any(|t| t == token);
168 Ok(Verdict {
169 ok,
170 reason: (!ok).then(|| "token not allowed".to_string()),
171 })
172 }
173
174 fn binding(&self) -> Option<cratefield_core::CaptchaBinding> {
175 Some(cratefield_core::CaptchaBinding {
176 hostname_bound: true,
177 action_bound: true,
178 fail_open: false,
179 })
180 }
181}
182
183#[derive(Clone)]
187pub struct FakeRateLimiter {
188 inner: Arc<FakeRateLimiterInner>,
189}
190
191struct FakeRateLimiterInner {
192 scripted: Mutex<VecDeque<Decision>>,
193 default: Decision,
194 calls: AtomicUsize,
195}
196
197impl FakeRateLimiter {
198 #[must_use]
200 pub fn scripted(decisions: Vec<Decision>, default: Decision) -> Self {
201 Self {
202 inner: Arc::new(FakeRateLimiterInner {
203 scripted: Mutex::new(decisions.into_iter().collect()),
204 default,
205 calls: AtomicUsize::new(0),
206 }),
207 }
208 }
209
210 #[must_use]
212 pub fn always_allow() -> Self {
213 Self::scripted(
214 Vec::new(),
215 Decision {
216 ok: true,
217 retry_after: None,
218 quota: None,
219 },
220 )
221 }
222
223 #[must_use]
224 pub fn calls(&self) -> usize {
225 self.inner.calls.load(Ordering::SeqCst)
226 }
227}
228
229#[async_trait]
230impl RateLimiter for FakeRateLimiter {
231 async fn limit(&self, _key: &str) -> Result<Decision, RateLimitError> {
232 self.inner.calls.fetch_add(1, Ordering::SeqCst);
233 let scripted = self
234 .inner
235 .scripted
236 .lock()
237 .expect("limiter lock")
238 .pop_front();
239 Ok(scripted.unwrap_or_else(|| self.inner.default.clone()))
240 }
241}
242
243#[derive(Debug, Clone)]
247pub struct FixedClock(pub time::OffsetDateTime);
248
249#[async_trait]
250impl Clock for FixedClock {
251 fn now(&self) -> time::OffsetDateTime {
252 self.0
253 }
254}
255
256#[derive(Debug, Clone)]
264pub struct ManualClock {
265 at: Arc<Mutex<time::OffsetDateTime>>,
266}
267
268impl ManualClock {
269 #[must_use]
271 pub fn new(at: time::OffsetDateTime) -> Self {
272 Self {
273 at: Arc::new(Mutex::new(at)),
274 }
275 }
276
277 pub fn advance(&self, by: Duration) {
280 let span = time::Duration::try_from(by).expect("a representable span");
281 *self.at.lock().expect("clock lock") += span;
282 }
283}
284
285#[async_trait]
286impl Clock for ManualClock {
287 fn now(&self) -> time::OffsetDateTime {
288 *self.at.lock().expect("clock lock")
289 }
290}
291
292#[derive(Clone, Default)]
296pub struct MemoryKeyValue {
297 inner: Arc<MemoryKeyValueInner>,
298}
299
300#[derive(Default)]
301struct MemoryKeyValueInner {
302 entries: Mutex<HashMap<String, String>>,
303}
304
305impl MemoryKeyValue {
306 #[must_use]
307 pub fn new() -> Self {
308 Self::default()
309 }
310}
311
312#[async_trait]
313impl KeyValue for MemoryKeyValue {
314 async fn get(&self, key: &str) -> Result<Option<String>, KvError> {
315 Ok(self
316 .inner
317 .entries
318 .lock()
319 .expect("kv lock")
320 .get(key)
321 .cloned())
322 }
323
324 async fn put(&self, key: &str, value: &str, _ttl: Option<Duration>) -> Result<(), KvError> {
325 self.inner
326 .entries
327 .lock()
328 .expect("kv lock")
329 .insert(key.to_string(), value.to_string());
330 Ok(())
331 }
332
333 async fn delete(&self, key: &str) -> Result<(), KvError> {
334 self.inner.entries.lock().expect("kv lock").remove(key);
335 Ok(())
336 }
337}
338
339#[derive(Clone)]
343pub struct FakeHttpClient {
344 inner: Arc<FakeHttpInner>,
345}
346
347struct FakeHttpInner {
348 responses: Mutex<VecDeque<Result<Response<Bytes>, HttpError>>>,
349 captured: Mutex<Vec<(String, String, String)>>, }
351
352impl FakeHttpClient {
353 #[must_use]
355 pub fn scripted(responses: Vec<Result<Response<Bytes>, HttpError>>) -> Self {
356 Self {
357 inner: Arc::new(FakeHttpInner {
358 responses: Mutex::new(responses.into_iter().collect()),
359 captured: Mutex::new(Vec::new()),
360 }),
361 }
362 }
363
364 #[must_use]
365 pub fn ok_json(body: &'static str) -> Self {
366 Self::scripted(vec![
367 Response::builder()
368 .status(200)
369 .body(Bytes::from(body))
370 .map_err(|err| HttpError::Transport(err.to_string())),
371 ])
372 }
373
374 #[must_use]
376 pub fn owlpost_problem(status: StatusCode, title: &str, detail: &str) -> Self {
377 Self::scripted(vec![Ok(problem_json(status, title, detail))])
378 }
379
380 #[must_use]
383 pub fn owlpost_rate_limited(retry_after_secs: u64) -> Self {
384 Self::scripted(vec![Ok(with_retry_after(
385 problem_json(
386 StatusCode::TOO_MANY_REQUESTS,
387 "Too Many Requests",
388 "slow down",
389 ),
390 retry_after_secs,
391 ))])
392 }
393
394 #[must_use]
396 pub fn colonizer_error(status: StatusCode, message: &str) -> Self {
397 Self::scripted(vec![Ok(error_json(status, message))])
398 }
399
400 #[must_use]
402 pub fn colonizer_rate_limited(retry_after_secs: u64) -> Self {
403 Self::scripted(vec![Ok(with_retry_after(
404 error_json(StatusCode::TOO_MANY_REQUESTS, "rate limited"),
405 retry_after_secs,
406 ))])
407 }
408
409 #[must_use]
411 pub fn captured(&self) -> Vec<(String, String, String)> {
412 self.inner.captured.lock().expect("http lock").clone()
413 }
414}
415
416#[must_use]
422pub fn problem_json(status: StatusCode, title: &str, detail: &str) -> Response<Bytes> {
423 let body = serde_json::json!({
424 "type": "about:blank",
425 "title": title,
426 "status": status.as_u16(),
427 "detail": detail,
428 })
429 .to_string();
430 Response::builder()
431 .status(status)
432 .header(CONTENT_TYPE, "application/problem+json")
433 .body(Bytes::from(body))
434 .expect("a problem+json response builds")
435}
436
437#[must_use]
440pub fn error_json(status: StatusCode, message: &str) -> Response<Bytes> {
441 let body = serde_json::json!({ "error": message }).to_string();
442 Response::builder()
443 .status(status)
444 .header(CONTENT_TYPE, "application/json")
445 .body(Bytes::from(body))
446 .expect("an error response builds")
447}
448
449#[must_use]
452pub fn with_retry_after(mut response: Response<Bytes>, retry_after_secs: u64) -> Response<Bytes> {
453 response
454 .headers_mut()
455 .insert(RETRY_AFTER, HeaderValue::from(retry_after_secs));
456 response
457}
458
459#[async_trait]
460impl HttpClient for FakeHttpClient {
461 async fn send(&self, request: Request<Bytes>) -> Result<Response<Bytes>, HttpError> {
462 let (parts, body) = request.into_parts();
463 self.inner.captured.lock().expect("http lock").push((
464 parts.method.to_string(),
465 parts.uri.to_string(),
466 String::from_utf8_lossy(&body).to_string(),
467 ));
468 let next = self.inner.responses.lock().expect("http lock").pop_front();
469 next.unwrap_or_else(|| Err(HttpError::Transport("fake http exhausted".to_string())))
470 }
471}
472
473#[derive(Clone, Default)]
477pub struct FakeDefer {
478 inner: Arc<FakeDeferInner>,
479}
480
481#[derive(Default)]
482struct FakeDeferInner {
483 pending: Mutex<Vec<BoxFuture<'static, ()>>>,
484 deferred: AtomicUsize,
485}
486
487impl FakeDefer {
488 #[must_use]
489 pub fn new() -> Self {
490 Self::default()
491 }
492
493 #[allow(clippy::unused_async, clippy::unused_async_trait_impl)]
497 pub async fn drain(&self) {
498 while !self.inner.pending.lock().expect("defer lock").is_empty() {
499 let next = self.inner.pending.lock().expect("defer lock").remove(0);
500 pollster::block_on(next);
501 }
502 }
503
504 #[must_use]
505 pub fn deferred_count(&self) -> usize {
506 self.inner.deferred.load(Ordering::SeqCst)
507 }
508}
509
510impl Defer for FakeDefer {
511 fn wait_until(&self, fut: BoxFuture<'static, ()>) {
512 self.inner.deferred.fetch_add(1, Ordering::SeqCst);
513 self.inner.pending.lock().expect("defer lock").push(fut);
514 }
515}
516
517#[derive(Debug, Clone, Copy, Default)]
522pub struct EmptyDatabase;
523
524#[async_trait]
525impl Database for EmptyDatabase {
526 async fn execute(&self, stmt: &Statement) -> Result<u64, DbError> {
527 Err(DbError::Execute(format!("empty database: {}", stmt.sql)))
528 }
529
530 async fn query(&self, stmt: &Statement) -> Result<Rows, DbError> {
531 if stmt.sql.trim() == "SELECT 1" {
532 Ok(Rows::new(vec![Row::new(vec![(
533 "1".to_string(),
534 sea_query::Value::Int(Some(1)),
535 )])]))
536 } else {
537 Err(DbError::Query(format!("empty database: {}", stmt.sql)))
538 }
539 }
540
541 async fn batch_atomic(&self, _stmts: &[Statement]) -> Result<(), DbError> {
542 Err(DbError::Batch("empty database".to_string()))
543 }
544}
545
546#[derive(Clone)]
556pub struct FakeDispatcher {
557 binding: String,
558 behaviour: FakeDispatch,
559 calls: Arc<AtomicUsize>,
560}
561
562#[derive(Clone)]
563enum FakeDispatch {
564 Serve(Arc<Mutex<axum::Router>>),
565 Unbound,
566 Failing(String),
567}
568
569impl FakeDispatcher {
570 #[must_use]
572 pub fn serving(binding: impl Into<String>, router: axum::Router) -> Self {
573 Self {
574 binding: binding.into(),
575 behaviour: FakeDispatch::Serve(Arc::new(Mutex::new(router))),
576 calls: Arc::new(AtomicUsize::new(0)),
577 }
578 }
579
580 #[must_use]
583 pub fn unbound() -> Self {
584 Self {
585 binding: String::new(),
586 behaviour: FakeDispatch::Unbound,
587 calls: Arc::new(AtomicUsize::new(0)),
588 }
589 }
590
591 #[must_use]
593 pub fn failing(binding: impl Into<String>, reason: impl Into<String>) -> Self {
594 Self {
595 binding: binding.into(),
596 behaviour: FakeDispatch::Failing(reason.into()),
597 calls: Arc::new(AtomicUsize::new(0)),
598 }
599 }
600
601 #[must_use]
604 pub fn calls(&self) -> usize {
605 self.calls.load(Ordering::SeqCst)
606 }
607}
608
609#[async_trait]
610impl cratefield_core::Dispatcher for FakeDispatcher {
611 fn has(&self, binding: &str) -> bool {
612 !matches!(self.behaviour, FakeDispatch::Unbound) && binding == self.binding
613 }
614
615 async fn dispatch(
616 &self,
617 binding: &str,
618 request: Request<Bytes>,
619 ) -> Result<Response<Bytes>, cratefield_core::DispatchError> {
620 self.calls.fetch_add(1, Ordering::SeqCst);
621 match &self.behaviour {
622 FakeDispatch::Unbound => {
623 Err(cratefield_core::DispatchError::NotBound(binding.to_owned()))
624 }
625 FakeDispatch::Failing(reason) => Err(cratefield_core::DispatchError::Unavailable {
626 binding: binding.to_owned(),
627 reason: reason.clone(),
628 }),
629 FakeDispatch::Serve(router) => {
630 let router = router.lock().unwrap().clone();
631 let (parts, body) = request.into_parts();
632 let request = Request::from_parts(parts, axum::body::Body::from(body));
633 let response =
634 tower::ServiceExt::oneshot(router, request)
635 .await
636 .map_err(|err| cratefield_core::DispatchError::Unavailable {
637 binding: binding.to_owned(),
638 reason: err.to_string(),
639 })?;
640 let (parts, body) = response.into_parts();
641 let bytes = axum::body::to_bytes(body, usize::MAX)
642 .await
643 .map_err(|err| cratefield_core::DispatchError::Unavailable {
644 binding: binding.to_owned(),
645 reason: err.to_string(),
646 })?;
647 Ok(Response::from_parts(parts, bytes))
648 }
649 }
650 }
651}
652
653#[derive(Clone, Default)]
658pub struct MemoryBlob {
659 objects: Arc<std::sync::Mutex<std::collections::HashMap<String, cratefield_core::BlobObject>>>,
660 uploads: Arc<std::sync::Mutex<std::collections::HashMap<String, MemoryUpload>>>,
662 next_upload_id: Arc<AtomicUsize>,
665 presign_clock: Option<Arc<dyn Clock>>,
667}
668
669struct MemoryUpload {
673 key: String,
674 content_type: String,
675 parts: BTreeMap<u16, (String, Vec<u8>)>,
676}
677
678async fn collect_capped(
684 mut body: cratefield_core::BoxStream<'static, Result<Bytes, cratefield_core::StreamError>>,
685 max_bytes: u64,
686) -> Result<(u64, Vec<u8>), cratefield_core::BlobError> {
687 let mut written: u64 = 0;
688 let mut out = Vec::new();
689 while let Some(chunk) = std::future::poll_fn(|cx| body.as_mut().poll_next(cx)).await {
690 let chunk = chunk.map_err(cratefield_core::blob_error_from_stream)?;
691 written = written.saturating_add(chunk.len() as u64);
692 if written > max_bytes {
693 return Err(cratefield_core::BlobError::TooLarge(format!(
694 "streamed body of {written} bytes passed the {max_bytes}-byte bound"
695 )));
696 }
697 out.extend_from_slice(&chunk);
698 }
699 Ok((written, out))
700}
701
702fn stored_body_stream(
705 bytes: &[u8],
706) -> cratefield_core::BoxStream<'static, Result<Bytes, cratefield_core::StreamError>> {
707 const CHUNK: usize = 1 << 20;
708 let chunks: Vec<Result<Bytes, cratefield_core::StreamError>> = bytes
709 .chunks(CHUNK)
710 .map(|chunk| Ok(Bytes::copy_from_slice(chunk)))
711 .collect();
712 Box::pin(ChunkStream {
713 chunks: chunks.into_iter(),
714 })
715}
716
717struct ChunkStream {
720 chunks: std::vec::IntoIter<Result<Bytes, cratefield_core::StreamError>>,
721}
722
723impl Stream for ChunkStream {
724 type Item = Result<Bytes, cratefield_core::StreamError>;
725
726 fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
727 Poll::Ready(self.get_mut().chunks.next())
728 }
729}
730
731const PRESIGN_ACCESS_KEY_ID: &str = "AKIATESTACCESSKEY000000";
734const PRESIGN_SECRET_ACCESS_KEY: &str = "memory-blob-presign-test-secret";
735const PRESIGN_HOST: &str = "blob.test";
736
737impl MemoryBlob {
738 #[must_use]
739 pub fn new() -> Self {
740 Self::default()
741 }
742
743 #[must_use]
747 pub fn with_presign(clock: Arc<dyn Clock>) -> Self {
748 Self {
749 presign_clock: Some(clock),
750 ..Self::default()
751 }
752 }
753
754 #[must_use]
756 pub fn len(&self) -> usize {
757 self.objects.lock().unwrap().len()
758 }
759
760 #[must_use]
762 pub fn is_empty(&self) -> bool {
763 self.len() == 0
764 }
765
766 fn presign(
769 &self,
770 method: &str,
771 key: &str,
772 headers: &[(String, String)],
773 ttl: Duration,
774 ) -> Result<String, cratefield_core::BlobError> {
775 let clock = self
776 .presign_clock
777 .as_ref()
778 .ok_or_else(|| cratefield_core::BlobError::Unsupported("no presigner".to_owned()))?;
779 let path = format!("/{key}");
780 let request = SignableRequest {
781 method,
782 host: PRESIGN_HOST,
783 path: &path,
784 query: &[],
785 headers,
786 };
787 Ok(sigv4::presign(
788 &Credentials::new(PRESIGN_ACCESS_KEY_ID, PRESIGN_SECRET_ACCESS_KEY),
789 "auto",
790 "s3",
791 &request,
792 ttl.as_secs(),
793 clock.now(),
794 ))
795 }
796
797 pub fn verify_presigned(
805 &self,
806 url: &str,
807 method: &str,
808 headers: &[(String, String)],
809 ) -> Result<String, SigV4Error> {
810 let clock = self
811 .presign_clock
812 .as_ref()
813 .ok_or_else(|| SigV4Error::Malformed("no presigning clock".to_owned()))?;
814 let credentials = Credentials::new(PRESIGN_ACCESS_KEY_ID, PRESIGN_SECRET_ACCESS_KEY);
815 sigv4::verify_presigned(
816 url,
817 method,
818 headers,
819 &credentials,
820 "auto",
821 "s3",
822 clock.now(),
823 )?;
824 url.split_once("://")
825 .and_then(|(_, rest)| rest.split_once('/'))
826 .map(|(_, path)| path.split(['?', '#']).next().unwrap_or(path).to_owned())
827 .ok_or_else(|| SigV4Error::Malformed("not an absolute URL".to_owned()))
828 }
829}
830
831#[async_trait]
832impl cratefield_core::Blob for MemoryBlob {
833 async fn put(
834 &self,
835 key: &str,
836 bytes: &[u8],
837 content_type: &str,
838 ) -> Result<(), cratefield_core::BlobError> {
839 cratefield_core::check_blob_size(bytes)?;
840 self.objects.lock().unwrap().insert(
841 key.to_owned(),
842 cratefield_core::BlobObject {
843 bytes: bytes.to_vec(),
844 content_type: content_type.to_owned(),
845 },
846 );
847 Ok(())
848 }
849 async fn get(
850 &self,
851 key: &str,
852 ) -> Result<Option<cratefield_core::BlobObject>, cratefield_core::BlobError> {
853 Ok(self.objects.lock().unwrap().get(key).cloned())
854 }
855 async fn delete(&self, key: &str) -> Result<(), cratefield_core::BlobError> {
856 self.objects.lock().unwrap().remove(key);
857 Ok(())
858 }
859 async fn signed_url(
860 &self,
861 key: &str,
862 ttl: std::time::Duration,
863 ) -> Result<String, cratefield_core::BlobError> {
864 self.presign("GET", key, &[], ttl)
865 }
866 async fn signed_put_url(
867 &self,
868 key: &str,
869 content_type: &str,
870 content_length: Option<u64>,
871 ttl: std::time::Duration,
872 ) -> Result<cratefield_core::PresignedPut, cratefield_core::BlobError> {
873 let mut headers = vec![("content-type".to_owned(), content_type.to_owned())];
874 if let Some(length) = content_length {
875 headers.push(("content-length".to_owned(), length.to_string()));
876 }
877 Ok(cratefield_core::PresignedPut {
878 url: self.presign("PUT", key, &headers, ttl)?,
879 method: "PUT",
880 headers,
881 })
882 }
883 async fn put_stream(
884 &self,
885 key: &str,
886 body: cratefield_core::BoxStream<'static, Result<Bytes, cratefield_core::StreamError>>,
887 content_type: &str,
888 max_bytes: u64,
889 ) -> Result<u64, cratefield_core::BlobError> {
890 let (written, bytes) = collect_capped(body, max_bytes).await?;
891 self.objects.lock().unwrap().insert(
892 key.to_owned(),
893 cratefield_core::BlobObject {
894 bytes,
895 content_type: content_type.to_owned(),
896 },
897 );
898 Ok(written)
899 }
900 async fn get_stream(
901 &self,
902 key: &str,
903 ) -> Result<Option<cratefield_core::BlobStream>, cratefield_core::BlobError> {
904 let Some(object) = self.objects.lock().unwrap().get(key).cloned() else {
905 return Ok(None);
906 };
907 let size = object.bytes.len() as u64;
908 Ok(Some(cratefield_core::BlobStream {
909 content_type: object.content_type,
910 size,
911 body: stored_body_stream(&object.bytes),
912 }))
913 }
914 async fn head(
915 &self,
916 key: &str,
917 ) -> Result<Option<cratefield_core::BlobMeta>, cratefield_core::BlobError> {
918 Ok(self
919 .objects
920 .lock()
921 .unwrap()
922 .get(key)
923 .map(|object| cratefield_core::BlobMeta {
924 key: key.to_owned(),
925 size: object.bytes.len() as u64,
926 content_type: object.content_type.clone(),
927 }))
928 }
929 async fn list(
930 &self,
931 prefix: &str,
932 cursor: Option<&str>,
933 limit: usize,
934 ) -> Result<cratefield_core::BlobPage, cratefield_core::BlobError> {
935 let objects = self.objects.lock().unwrap();
936 let mut keys: Vec<String> = objects
937 .keys()
938 .filter(|key| key.starts_with(prefix))
939 .cloned()
940 .collect();
941 keys.sort();
942 let start = cursor.map_or(0, |cursor| {
943 keys.partition_point(|key| key.as_str() <= cursor)
944 });
945 let take = (keys.len() - start).min(limit);
946 let selected = &keys[start..start + take];
947 let page = selected
948 .iter()
949 .map(|key| {
950 let object = &objects[key];
951 cratefield_core::BlobMeta {
952 key: key.clone(),
953 size: object.bytes.len() as u64,
954 content_type: object.content_type.clone(),
955 }
956 })
957 .collect();
958 let cursor = if start + take < keys.len() {
959 selected.last().cloned()
960 } else {
961 None
962 };
963 Ok(cratefield_core::BlobPage {
964 objects: page,
965 cursor,
966 })
967 }
968 async fn create_multipart(
969 &self,
970 key: &str,
971 content_type: &str,
972 ) -> Result<cratefield_core::UploadId, cratefield_core::BlobError> {
973 let id = self.next_upload_id.fetch_add(1, Ordering::Relaxed) + 1;
974 let upload_id = cratefield_core::UploadId::new(format!("upload-{id}"));
975 self.uploads.lock().unwrap().insert(
976 upload_id.as_str().to_owned(),
977 MemoryUpload {
978 key: key.to_owned(),
979 content_type: content_type.to_owned(),
980 parts: BTreeMap::new(),
981 },
982 );
983 Ok(upload_id)
984 }
985 async fn upload_part(
986 &self,
987 key: &str,
988 upload_id: &cratefield_core::UploadId,
989 part_number: u16,
990 body: cratefield_core::BoxStream<'static, Result<Bytes, cratefield_core::StreamError>>,
991 max_part_bytes: u64,
992 ) -> Result<cratefield_core::PartReceipt, cratefield_core::BlobError> {
993 cratefield_core::check_part_number(part_number)?;
994 let (_, bytes) = collect_capped(body, max_part_bytes).await?;
995 let mut uploads = self.uploads.lock().unwrap();
996 let upload = uploads.get_mut(upload_id.as_str()).ok_or_else(|| {
997 cratefield_core::BlobError::Operation(format!(
998 "no multipart upload `{}`",
999 upload_id.as_str()
1000 ))
1001 })?;
1002 if upload.key != key {
1003 return Err(cratefield_core::BlobError::Operation(
1004 "multipart upload belongs to a different key".to_owned(),
1005 ));
1006 }
1007 let etag = format!("part-{part_number}-{}", bytes.len());
1008 upload.parts.insert(part_number, (etag.clone(), bytes));
1009 Ok(cratefield_core::PartReceipt { part_number, etag })
1010 }
1011 async fn complete_multipart(
1012 &self,
1013 key: &str,
1014 upload_id: &cratefield_core::UploadId,
1015 parts: &[cratefield_core::PartReceipt],
1016 ) -> Result<(), cratefield_core::BlobError> {
1017 let mut uploads = self.uploads.lock().unwrap();
1018 let assembled = {
1022 let upload = uploads.get(upload_id.as_str()).ok_or_else(|| {
1023 cratefield_core::BlobError::Operation(format!(
1024 "no multipart upload `{}`",
1025 upload_id.as_str()
1026 ))
1027 })?;
1028 if upload.key != key {
1029 return Err(cratefield_core::BlobError::Operation(
1030 "multipart upload belongs to a different key".to_owned(),
1031 ));
1032 }
1033 let mut ordered = parts.to_vec();
1034 ordered.sort_by_key(|receipt| receipt.part_number);
1035 let mut assembled = Vec::new();
1036 for (index, receipt) in ordered.iter().enumerate() {
1037 let (etag, bytes) = upload.parts.get(&receipt.part_number).ok_or_else(|| {
1038 cratefield_core::BlobError::Operation(format!(
1039 "part {} was never uploaded",
1040 receipt.part_number
1041 ))
1042 })?;
1043 if *etag != receipt.etag {
1044 return Err(cratefield_core::BlobError::Operation(format!(
1045 "part {} etag mismatch",
1046 receipt.part_number
1047 )));
1048 }
1049 let is_last = index + 1 == ordered.len();
1050 if !is_last && (bytes.len() as u64) < cratefield_core::MIN_MULTIPART_PART_BYTES {
1051 return Err(cratefield_core::BlobError::Operation(format!(
1052 "part {} is below the {}-byte minimum for a non-final part",
1053 receipt.part_number,
1054 cratefield_core::MIN_MULTIPART_PART_BYTES,
1055 )));
1056 }
1057 assembled.extend_from_slice(bytes);
1058 }
1059 assembled
1060 };
1061 let upload = uploads
1062 .remove(upload_id.as_str())
1063 .expect("the upload was checked above");
1064 let content_type = upload.content_type;
1065 drop(uploads);
1066 self.objects.lock().unwrap().insert(
1067 key.to_owned(),
1068 cratefield_core::BlobObject {
1069 bytes: assembled,
1070 content_type,
1071 },
1072 );
1073 Ok(())
1074 }
1075 async fn abort_multipart(
1076 &self,
1077 key: &str,
1078 upload_id: &cratefield_core::UploadId,
1079 ) -> Result<(), cratefield_core::BlobError> {
1080 let mut uploads = self.uploads.lock().unwrap();
1081 let upload = uploads.remove(upload_id.as_str()).ok_or_else(|| {
1082 cratefield_core::BlobError::Operation(format!(
1083 "no multipart upload `{}`",
1084 upload_id.as_str()
1085 ))
1086 })?;
1087 if upload.key != key {
1088 uploads.insert(upload_id.as_str().to_owned(), upload);
1089 return Err(cratefield_core::BlobError::Operation(
1090 "multipart upload belongs to a different key".to_owned(),
1091 ));
1092 }
1093 Ok(())
1094 }
1095 async fn list_multipart_uploads(
1096 &self,
1097 prefix: &str,
1098 ) -> Result<Vec<cratefield_core::PendingUpload>, cratefield_core::BlobError> {
1099 let uploads = self.uploads.lock().unwrap();
1100 let mut pending: Vec<cratefield_core::PendingUpload> = uploads
1101 .iter()
1102 .filter(|(_, upload)| upload.key.starts_with(prefix))
1103 .map(|(id, upload)| cratefield_core::PendingUpload {
1104 key: upload.key.clone(),
1105 upload_id: cratefield_core::UploadId::new(id.clone()),
1106 })
1107 .collect();
1108 pending.sort_by(|a, b| a.key.cmp(&b.key));
1109 Ok(pending)
1110 }
1111}
1112
1113const MAX_ALARMS_PER_ADVANCE: usize = 4;
1120
1121#[derive(Clone)]
1131pub struct MemoryActors {
1132 inner: Arc<MemoryActorsInner>,
1133}
1134
1135struct MemoryActorsInner {
1136 handlers: ActorHandlers,
1137 clock: ManualClock,
1138 actors: Mutex<HashMap<(String, String), Arc<ActorState>>>,
1141}
1142
1143#[derive(Default)]
1146struct ActorState {
1147 store: MemoryActorStore,
1148 lock: futures_util::lock::Mutex<()>,
1149}
1150
1151#[derive(Default)]
1155struct MemoryActorStore {
1156 state: Mutex<MemoryActorState>,
1157}
1158
1159#[derive(Default)]
1160struct MemoryActorState {
1161 values: BTreeMap<String, Vec<u8>>,
1162 alarm: Option<time::OffsetDateTime>,
1163}
1164
1165#[async_trait]
1166impl ActorStore for MemoryActorStore {
1167 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, ActorError> {
1168 Ok(self
1169 .state
1170 .lock()
1171 .expect("store lock")
1172 .values
1173 .get(key)
1174 .cloned())
1175 }
1176
1177 async fn list_prefix(&self, prefix: &str) -> Result<Vec<(String, Vec<u8>)>, ActorError> {
1178 Ok(self
1179 .state
1180 .lock()
1181 .expect("store lock")
1182 .values
1183 .iter()
1184 .filter(|(key, _)| key.starts_with(prefix))
1185 .map(|(key, value)| (key.clone(), value.clone()))
1186 .collect())
1187 }
1188
1189 async fn alarm(&self) -> Result<Option<time::OffsetDateTime>, ActorError> {
1190 Ok(self.state.lock().expect("store lock").alarm)
1191 }
1192
1193 async fn commit(&self, writes: ActorWrites) -> Result<(), ActorError> {
1194 let mut state = self.state.lock().expect("store lock");
1195 if writes.clear_all {
1196 state.values.clear();
1197 }
1198 for (key, value) in writes.entries {
1199 match value {
1200 Some(value) => {
1201 state.values.insert(key, value);
1202 }
1203 None => {
1204 state.values.remove(&key);
1205 }
1206 }
1207 }
1208 if let Some(alarm) = writes.alarm {
1209 state.alarm = alarm;
1210 }
1211 Ok(())
1212 }
1213}
1214
1215impl MemoryActors {
1216 #[must_use]
1219 pub fn new(handlers: ActorHandlers, clock: ManualClock) -> Self {
1220 Self {
1221 inner: Arc::new(MemoryActorsInner {
1222 handlers,
1223 clock,
1224 actors: Mutex::new(HashMap::new()),
1225 }),
1226 }
1227 }
1228
1229 fn actor(&self, kind: &str, key: &str) -> Arc<ActorState> {
1231 self.inner
1232 .actors
1233 .lock()
1234 .expect("actors lock")
1235 .entry((kind.to_owned(), key.to_owned()))
1236 .or_default()
1237 .clone()
1238 }
1239
1240 pub async fn advance(&self, by: Duration) {
1248 self.inner.clock.advance(by);
1249 let now = self.inner.clock.now();
1250 let actors: Vec<(String, String, Arc<ActorState>)> = {
1251 let registry = self.inner.actors.lock().expect("actors lock");
1252 registry
1253 .iter()
1254 .map(|((kind, key), state)| (kind.clone(), key.clone(), Arc::clone(state)))
1255 .collect()
1256 };
1257 for (kind, key, state) in actors {
1258 let Some(handler) = self.inner.handlers.get(&kind) else {
1259 continue;
1260 };
1261 for _ in 0..MAX_ALARMS_PER_ADVANCE {
1262 let _held = state.lock.lock().await;
1263 let due = matches!(state.store.alarm().await, Ok(Some(at)) if at <= now);
1264 if !due {
1265 break;
1266 }
1267 if run_actor_alarm(
1268 handler.as_ref(),
1269 &state.store,
1270 &self.inner.clock,
1271 &kind,
1272 &key,
1273 )
1274 .await
1275 .is_err()
1276 {
1277 break;
1278 }
1279 }
1280 }
1281 }
1282}
1283
1284#[async_trait]
1285impl Actors for MemoryActors {
1286 async fn call(&self, kind: &str, key: &str, message: &[u8]) -> Result<Vec<u8>, ActorError> {
1287 let Some(handler) = self.inner.handlers.get(kind) else {
1288 return Err(ActorError::NotConfigured);
1289 };
1290 let state = self.actor(kind, key);
1291 let _held = state.lock.lock().await;
1292 run_actor_message(
1293 handler.as_ref(),
1294 &state.store,
1295 &self.inner.clock,
1296 kind,
1297 key,
1298 message,
1299 )
1300 .await
1301 }
1302}
1303
1304pub const FAKE_EMBEDDER_DIMENSIONS: usize = 8;
1311
1312pub const FAKE_EMBEDDER_MODEL: &str = "fake-embedder";
1314
1315#[derive(Debug, Clone, Copy, Default)]
1319pub struct FakeEmbedder;
1320
1321impl FakeEmbedder {
1322 #[must_use]
1326 pub fn vector(text: &str) -> Vec<f32> {
1327 let mut out = vec![0.0; FAKE_EMBEDDER_DIMENSIONS];
1328 for (index, byte) in text.bytes().enumerate() {
1329 out[index % FAKE_EMBEDDER_DIMENSIONS] += f32::from(byte);
1330 }
1331 out
1332 }
1333}
1334
1335#[async_trait]
1336impl cratefield_core::Embedder for FakeEmbedder {
1337 async fn embed(
1338 &self,
1339 texts: &[String],
1340 ) -> Result<cratefield_core::Embeddings, cratefield_core::EmbedError> {
1341 let input_tokens = texts
1342 .iter()
1343 .map(|text| text.split_whitespace().count() as u64)
1344 .sum();
1345 let mut embeddings =
1346 cratefield_core::Embeddings::new(FAKE_EMBEDDER_MODEL).usage(input_tokens);
1347 for text in texts {
1348 embeddings = embeddings.vector(Self::vector(text));
1349 }
1350 Ok(embeddings)
1351 }
1352}
1353
1354#[derive(Debug, Clone, PartialEq, Eq)]
1364pub enum PushMode {
1365 DeliverOk,
1367 NotConfigured,
1369 Unregistered,
1371 Transient,
1373 Error(cratefield_core::PushError),
1375}
1376
1377#[derive(Clone)]
1385pub struct FakePush {
1386 inner: Arc<FakePushInner>,
1387}
1388
1389struct FakePushInner {
1390 mode: Mutex<PushMode>,
1391 per_recipient: Mutex<HashMap<cratefield_core::Recipient, PushMode>>,
1392 sent: Mutex<Vec<(cratefield_core::Recipient, cratefield_core::Notification)>>,
1393}
1394
1395impl FakePush {
1396 #[must_use]
1397 pub fn new(mode: PushMode) -> Self {
1398 Self {
1399 inner: Arc::new(FakePushInner {
1400 mode: Mutex::new(mode),
1401 per_recipient: Mutex::new(HashMap::new()),
1402 sent: Mutex::new(Vec::new()),
1403 }),
1404 }
1405 }
1406
1407 #[must_use]
1410 pub fn sent(&self) -> Vec<(cratefield_core::Recipient, cratefield_core::Notification)> {
1411 self.inner.sent.lock().expect("push lock").clone()
1412 }
1413
1414 #[must_use]
1416 pub fn last(&self) -> Option<(cratefield_core::Recipient, cratefield_core::Notification)> {
1417 self.inner.sent.lock().expect("push lock").last().cloned()
1418 }
1419
1420 #[must_use]
1422 pub fn sent_to(
1423 &self,
1424 recipient: &cratefield_core::Recipient,
1425 ) -> Vec<cratefield_core::Notification> {
1426 self.inner
1427 .sent
1428 .lock()
1429 .expect("push lock")
1430 .iter()
1431 .filter(|(to, _)| to == recipient)
1432 .map(|(_, notification)| notification.clone())
1433 .collect()
1434 }
1435
1436 pub fn set_mode(&self, mode: PushMode) {
1439 *self.inner.mode.lock().expect("push lock") = mode;
1440 }
1441
1442 pub fn set_mode_for(&self, recipient: &cratefield_core::Recipient, mode: PushMode) {
1445 self.inner
1446 .per_recipient
1447 .lock()
1448 .expect("push lock")
1449 .insert(recipient.clone(), mode);
1450 }
1451
1452 pub fn clear_mode_for(&self, recipient: &cratefield_core::Recipient) {
1454 self.inner
1455 .per_recipient
1456 .lock()
1457 .expect("push lock")
1458 .remove(recipient);
1459 }
1460
1461 #[must_use]
1463 pub fn mode_for(&self, recipient: &cratefield_core::Recipient) -> PushMode {
1464 self.inner
1465 .per_recipient
1466 .lock()
1467 .expect("push lock")
1468 .get(recipient)
1469 .cloned()
1470 .unwrap_or_else(|| self.inner.mode.lock().expect("push lock").clone())
1471 }
1472}
1473
1474impl Default for FakePush {
1475 fn default() -> Self {
1476 Self::new(PushMode::DeliverOk)
1477 }
1478}
1479
1480#[async_trait]
1481impl cratefield_core::Push for FakePush {
1482 async fn send(
1483 &self,
1484 to: &cratefield_core::Recipient,
1485 notification: &cratefield_core::Notification,
1486 ) -> Result<cratefield_core::PushOutcome, cratefield_core::PushError> {
1487 match self.mode_for(to) {
1488 PushMode::DeliverOk => {
1489 let id = format!(
1490 "fake-push-{}",
1491 self.inner.sent.lock().expect("push lock").len()
1492 );
1493 self.inner
1494 .sent
1495 .lock()
1496 .expect("push lock")
1497 .push((to.clone(), notification.clone()));
1498 Ok(cratefield_core::PushOutcome::Delivered { id: Some(id) })
1499 }
1500 PushMode::NotConfigured => Ok(cratefield_core::PushOutcome::NotConfigured),
1501 PushMode::Unregistered => Err(cratefield_core::PushError::Unregistered),
1502 PushMode::Transient => Err(cratefield_core::PushError::transient("fake push failure")),
1503 PushMode::Error(error) => Err(error),
1504 }
1505 }
1506}
1507
1508#[derive(Debug, Clone, PartialEq)]
1519pub enum TextModelMode {
1520 Reply(String),
1523 Complete(cratefield_core::Completion),
1525 NotConfigured,
1527 Rejected(String),
1529 Transient { retry_after: Option<Duration> },
1531 Error(cratefield_core::TextModelError),
1533}
1534
1535#[derive(Clone)]
1551pub struct FakeTextModel {
1552 inner: Arc<FakeTextModelInner>,
1553}
1554
1555struct FakeTextModelInner {
1556 mode: Mutex<TextModelMode>,
1557 per_tier: Mutex<HashMap<ModelTier, TextModelMode>>,
1558 prompts: Mutex<Vec<Prompt>>,
1559}
1560
1561fn fake_usage(prompt: &Prompt, text: &str) -> (u64, u64) {
1566 let prompt_chars = prompt.system.as_deref().map_or(0, str::len)
1567 + prompt
1568 .messages
1569 .iter()
1570 .map(|turn| turn.content.len())
1571 .sum::<usize>();
1572 let input = (prompt_chars / 4).max(1) as u64;
1573 let output = (text.len() / 4).max(1) as u64;
1574 (input, output)
1575}
1576
1577impl FakeTextModel {
1578 #[must_use]
1579 pub fn new(mode: TextModelMode) -> Self {
1580 Self {
1581 inner: Arc::new(FakeTextModelInner {
1582 mode: Mutex::new(mode),
1583 per_tier: Mutex::new(HashMap::new()),
1584 prompts: Mutex::new(Vec::new()),
1585 }),
1586 }
1587 }
1588
1589 #[must_use]
1592 pub fn prompts(&self) -> Vec<Prompt> {
1593 self.inner.prompts.lock().expect("text model lock").clone()
1594 }
1595
1596 #[must_use]
1598 pub fn last(&self) -> Option<Prompt> {
1599 self.inner
1600 .prompts
1601 .lock()
1602 .expect("text model lock")
1603 .last()
1604 .cloned()
1605 }
1606
1607 pub fn set_mode(&self, mode: TextModelMode) {
1610 *self.inner.mode.lock().expect("text model lock") = mode;
1611 }
1612
1613 pub fn set_mode_for(&self, tier: ModelTier, mode: TextModelMode) {
1616 self.inner
1617 .per_tier
1618 .lock()
1619 .expect("text model lock")
1620 .insert(tier, mode);
1621 }
1622
1623 pub fn clear_mode_for(&self, tier: ModelTier) {
1625 self.inner
1626 .per_tier
1627 .lock()
1628 .expect("text model lock")
1629 .remove(&tier);
1630 }
1631
1632 #[must_use]
1634 pub fn mode_for(&self, tier: ModelTier) -> TextModelMode {
1635 self.inner
1636 .per_tier
1637 .lock()
1638 .expect("text model lock")
1639 .get(&tier)
1640 .cloned()
1641 .unwrap_or_else(|| self.inner.mode.lock().expect("text model lock").clone())
1642 }
1643
1644 fn record(&self, prompt: &Prompt) {
1645 self.inner
1646 .prompts
1647 .lock()
1648 .expect("text model lock")
1649 .push(prompt.clone());
1650 }
1651}
1652
1653impl Default for FakeTextModel {
1654 fn default() -> Self {
1655 Self::new(TextModelMode::Reply("fake completion".to_owned()))
1656 }
1657}
1658
1659#[async_trait]
1660impl TextModel for FakeTextModel {
1661 async fn complete(&self, prompt: &Prompt) -> Result<Completion, TextModelError> {
1662 if !prompt.tools.is_empty() {
1667 return Err(TextModelError::Unsupported(Capability::Tools));
1668 }
1669 prompt.check_images()?;
1673 match self.mode_for(prompt.tier) {
1674 TextModelMode::Reply(text) => {
1675 self.record(prompt);
1676 let (input_tokens, output_tokens) = fake_usage(prompt, &text);
1677 Ok(
1678 Completion::new(text, format!("fake-{}", prompt.tier.name()))
1679 .usage(input_tokens, output_tokens),
1680 )
1681 }
1682 TextModelMode::Complete(completion) => {
1683 self.record(prompt);
1684 Ok(completion)
1685 }
1686 TextModelMode::NotConfigured => Err(TextModelError::NotConfigured),
1687 TextModelMode::Rejected(message) => Err(TextModelError::Rejected(message)),
1688 TextModelMode::Transient { retry_after } => {
1689 Err(TextModelError::Transient { retry_after })
1690 }
1691 TextModelMode::Error(error) => Err(error),
1692 }
1693 }
1694
1695 fn supports(&self, _tier: ModelTier, capability: Capability) -> bool {
1696 capability == Capability::Images
1701 }
1702}
1703
1704#[derive(Debug, Clone, PartialEq)]
1716pub enum ClassifierMode {
1717 Answers(BTreeMap<String, Answer>),
1721 Deterministic,
1726 NotConfigured,
1728 Rejected(String),
1730 Transient { retry_after: Option<Duration> },
1732 Error(ClassifierError),
1734}
1735
1736#[derive(Debug, Clone, PartialEq, Eq)]
1740pub struct Ask {
1741 pub state: String,
1743 pub question_ids: Vec<String>,
1745}
1746
1747#[derive(Clone)]
1761pub struct FakeClassifier {
1762 inner: Arc<FakeClassifierInner>,
1763}
1764
1765struct FakeClassifierInner {
1766 mode: Mutex<ClassifierMode>,
1767 per_question: Mutex<BTreeMap<String, Answer>>,
1768 asks: Mutex<Vec<Ask>>,
1769 profile: Mutex<ClassifierProfile>,
1770}
1771
1772fn deterministic_answer(question: &Question) -> Answer {
1778 let labels = question.labels();
1779 let chosen = labels.first().copied().unwrap_or_default();
1780 let share = if labels.len() > 1 {
1783 0.4 / f32::from(u16::try_from(labels.len() - 1).unwrap_or(1))
1784 } else {
1785 0.0
1786 };
1787 let probabilities: BTreeMap<String, f32> = labels
1788 .iter()
1789 .map(|label| {
1790 let mass = if *label == chosen { 0.6 } else { share };
1791 ((*label).to_owned(), mass)
1792 })
1793 .collect();
1794 match question {
1795 Question::Choice { .. } => Answer::choice(chosen, probabilities),
1796 Question::Noul { .. } => Answer::noul(chosen == "true", probabilities),
1797 Question::Score { .. } => Answer::new(
1801 AnswerValue::Score(chosen.parse().unwrap_or_default()),
1802 probabilities,
1803 0.6,
1804 ),
1805 }
1806}
1807
1808impl FakeClassifier {
1809 #[must_use]
1810 pub fn new(mode: ClassifierMode) -> Self {
1811 Self {
1812 inner: Arc::new(FakeClassifierInner {
1813 mode: Mutex::new(mode),
1814 per_question: Mutex::new(BTreeMap::new()),
1815 asks: Mutex::new(Vec::new()),
1816 profile: Mutex::new(ClassifierProfile::new(
1817 Calibration::Classifier,
1818 cratefield_core::DEFAULT_MAX_STATE_CHARS,
1819 )),
1820 }),
1821 }
1822 }
1823
1824 #[must_use]
1827 pub fn asks(&self) -> Vec<Ask> {
1828 self.inner.asks.lock().expect("classifier lock").clone()
1829 }
1830
1831 #[must_use]
1833 pub fn last(&self) -> Option<Ask> {
1834 self.inner
1835 .asks
1836 .lock()
1837 .expect("classifier lock")
1838 .last()
1839 .cloned()
1840 }
1841
1842 pub fn set_mode(&self, mode: ClassifierMode) {
1845 *self.inner.mode.lock().expect("classifier lock") = mode;
1846 }
1847
1848 pub fn set_answer_for(&self, question_id: impl Into<String>, answer: Answer) {
1852 self.inner
1853 .per_question
1854 .lock()
1855 .expect("classifier lock")
1856 .insert(question_id.into(), answer);
1857 }
1858
1859 pub fn clear_answer_for(&self, question_id: &str) {
1861 self.inner
1862 .per_question
1863 .lock()
1864 .expect("classifier lock")
1865 .remove(question_id);
1866 }
1867
1868 pub fn set_profile(&self, calibration: Calibration, max_state_chars: usize) {
1872 *self.inner.profile.lock().expect("classifier lock") =
1873 ClassifierProfile::new(calibration, max_state_chars);
1874 }
1875}
1876
1877impl Default for FakeClassifier {
1878 fn default() -> Self {
1879 Self::new(ClassifierMode::Deterministic)
1880 }
1881}
1882
1883#[async_trait]
1884impl Classifier for FakeClassifier {
1885 fn profile(&self) -> ClassifierProfile {
1886 *self.inner.profile.lock().expect("classifier lock")
1887 }
1888
1889 async fn ask(
1890 &self,
1891 state: &str,
1892 questions: &BTreeMap<String, Question>,
1893 ) -> Result<BTreeMap<String, Answer>, ClassifierError> {
1894 validate_questions(questions)?;
1897
1898 let mode = self.inner.mode.lock().expect("classifier lock").clone();
1899 match mode {
1900 ClassifierMode::NotConfigured => return Err(ClassifierError::NotConfigured),
1901 ClassifierMode::Rejected(message) => return Err(ClassifierError::Rejected(message)),
1902 ClassifierMode::Transient { retry_after } => {
1903 return Err(ClassifierError::Transient { retry_after });
1904 }
1905 ClassifierMode::Error(error) => return Err(error),
1906 ClassifierMode::Answers(_) | ClassifierMode::Deterministic => {}
1907 }
1908
1909 let overrides = self.inner.per_question.lock().expect("classifier lock");
1910 let answers: BTreeMap<String, Answer> = questions
1911 .iter()
1912 .map(|(id, question)| {
1913 let answer = overrides.get(id).cloned().unwrap_or_else(|| match &mode {
1914 ClassifierMode::Answers(map) => map
1915 .get(id)
1916 .cloned()
1917 .unwrap_or_else(|| deterministic_answer(question)),
1918 _ => deterministic_answer(question),
1919 });
1920 (id.clone(), answer)
1921 })
1922 .collect();
1923 drop(overrides);
1924
1925 self.inner.asks.lock().expect("classifier lock").push(Ask {
1926 state: state.to_owned(),
1927 question_ids: questions.keys().cloned().collect(),
1928 });
1929 Ok(answers)
1930 }
1931}
1932
1933#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1938pub enum PaymentsMode {
1939 Ok,
1941 NotConfigured,
1943 Transient,
1945}
1946
1947#[derive(Debug, Clone, PartialEq, Eq)]
1949pub enum PaymentsCall {
1950 Checkout,
1951 SubscriptionCheckout,
1952 ConnectAccountLink,
1953 ChargeWithTransfer,
1954 Refund,
1955 VerifyWebhook,
1956}
1957
1958#[derive(Clone)]
1962pub struct FakePayments {
1963 inner: Arc<FakePaymentsInner>,
1964}
1965
1966struct FakePaymentsInner {
1967 mode: Mutex<PaymentsMode>,
1968 calls: Mutex<Vec<PaymentsCall>>,
1969}
1970
1971impl FakePayments {
1972 #[must_use]
1973 pub fn new(mode: PaymentsMode) -> Self {
1974 Self {
1975 inner: Arc::new(FakePaymentsInner {
1976 mode: Mutex::new(mode),
1977 calls: Mutex::new(Vec::new()),
1978 }),
1979 }
1980 }
1981
1982 #[must_use]
1984 pub fn calls(&self) -> Vec<PaymentsCall> {
1985 self.inner.calls.lock().expect("payments lock").clone()
1986 }
1987
1988 pub fn set_mode(&self, mode: PaymentsMode) {
1989 *self.inner.mode.lock().expect("payments lock") = mode;
1990 }
1991
1992 fn record(&self, call: PaymentsCall) {
1993 self.inner.calls.lock().expect("payments lock").push(call);
1994 }
1995
1996 fn guard(&self) -> Result<(), cratefield_core::PaymentsError> {
1997 match *self.inner.mode.lock().expect("payments lock") {
1998 PaymentsMode::Ok => Ok(()),
1999 PaymentsMode::NotConfigured => Err(cratefield_core::PaymentsError::NotConfigured),
2000 PaymentsMode::Transient => Err(cratefield_core::PaymentsError::Transient(
2001 "fake payments failure".to_owned(),
2002 )),
2003 }
2004 }
2005}
2006
2007impl Default for FakePayments {
2008 fn default() -> Self {
2009 Self::new(PaymentsMode::Ok)
2010 }
2011}
2012
2013#[async_trait]
2014impl cratefield_core::Payments for FakePayments {
2015 async fn create_checkout(
2016 &self,
2017 _request: &cratefield_core::CheckoutRequest,
2018 ) -> Result<cratefield_core::CheckoutSession, cratefield_core::PaymentsError> {
2019 self.guard()?;
2020 self.record(PaymentsCall::Checkout);
2021 Ok(cratefield_core::CheckoutSession {
2022 id: "cs_fake".to_owned(),
2023 url: "https://checkout.stripe.test/cs_fake".to_owned(),
2024 })
2025 }
2026
2027 async fn create_subscription_checkout(
2028 &self,
2029 _request: &cratefield_core::SubscriptionCheckoutRequest,
2030 ) -> Result<cratefield_core::CheckoutSession, cratefield_core::PaymentsError> {
2031 self.guard()?;
2032 self.record(PaymentsCall::SubscriptionCheckout);
2033 Ok(cratefield_core::CheckoutSession {
2034 id: "cs_sub_fake".to_owned(),
2035 url: "https://checkout.stripe.test/cs_sub_fake".to_owned(),
2036 })
2037 }
2038
2039 async fn create_connect_account_link(
2040 &self,
2041 _request: &cratefield_core::ConnectAccountLinkRequest,
2042 ) -> Result<cratefield_core::ConnectAccountLink, cratefield_core::PaymentsError> {
2043 self.guard()?;
2044 self.record(PaymentsCall::ConnectAccountLink);
2045 Ok(cratefield_core::ConnectAccountLink {
2046 account_id: "acct_fake".to_owned(),
2047 url: "https://connect.stripe.test/acct_fake".to_owned(),
2048 })
2049 }
2050
2051 async fn charge_with_transfer(
2052 &self,
2053 _request: &cratefield_core::TransferCharge,
2054 ) -> Result<cratefield_core::Charge, cratefield_core::PaymentsError> {
2055 self.guard()?;
2056 self.record(PaymentsCall::ChargeWithTransfer);
2057 Ok(cratefield_core::Charge {
2058 id: "pi_fake".to_owned(),
2059 status: "succeeded".to_owned(),
2060 })
2061 }
2062
2063 async fn refund(
2064 &self,
2065 _request: &cratefield_core::RefundRequest,
2066 ) -> Result<cratefield_core::Refund, cratefield_core::PaymentsError> {
2067 self.guard()?;
2068 self.record(PaymentsCall::Refund);
2069 Ok(cratefield_core::Refund {
2070 id: "re_fake".to_owned(),
2071 })
2072 }
2073
2074 async fn verify_webhook(
2075 &self,
2076 signature_header: &str,
2077 _body: &[u8],
2078 ) -> Result<cratefield_core::WebhookEvent, cratefield_core::PaymentsError> {
2079 self.record(PaymentsCall::VerifyWebhook);
2080 if signature_header == "invalid" {
2081 return Err(cratefield_core::PaymentsError::SignatureInvalid(
2082 "fake tampered signature".to_owned(),
2083 ));
2084 }
2085 self.guard()?;
2086 Ok(cratefield_core::WebhookEvent {
2087 id: "evt_fake".to_owned(),
2088 kind: "checkout.session.completed".to_owned(),
2089 data: serde_json::json!({ "object": "checkout.session" }),
2090 })
2091 }
2092}
2093
2094fn fingerprint(credential: &Credential) -> String {
2110 use std::hash::{Hash, Hasher};
2111
2112 let mut hasher = std::hash::DefaultHasher::new();
2115 credential.expose().hash(&mut hasher);
2116 format!("fp:{:016x}", hasher.finish())
2117}
2118
2119#[derive(Debug, Clone, PartialEq, Eq)]
2128pub enum TrackerMode {
2129 FileOk,
2131 NotConfigured,
2133 Unauthorized,
2135 Rejected,
2138 Transient,
2140 Error(TrackerError),
2142}
2143
2144#[derive(Debug, Clone, PartialEq, Eq)]
2150pub struct FiledCall {
2151 pub dest: Destination,
2153 pub draft: TicketDraft,
2155 pub credential_fingerprint: String,
2157}
2158
2159#[derive(Debug, Clone, PartialEq, Eq)]
2161pub struct StatusedCall {
2162 pub dest: Destination,
2164 pub external_id: String,
2166 pub credential_fingerprint: String,
2168}
2169
2170#[derive(Debug, Clone, PartialEq, Eq)]
2172pub struct CommentedCall {
2173 pub dest: Destination,
2175 pub external_id: String,
2177 pub comment: TicketComment,
2179 pub credential_fingerprint: String,
2181}
2182
2183#[derive(Clone, Debug)]
2196pub struct FakeTracker {
2197 inner: Arc<FakeTrackerInner>,
2198}
2199
2200#[derive(Debug)]
2201struct FakeTrackerInner {
2202 mode: Mutex<TrackerMode>,
2203 per_destination: Mutex<HashMap<Destination, TrackerMode>>,
2204 filed: Mutex<Vec<FiledCall>>,
2205 statused: Mutex<Vec<StatusedCall>>,
2206 commented: Mutex<Vec<CommentedCall>>,
2207 state: Mutex<TicketState>,
2208}
2209
2210impl FakeTracker {
2211 #[must_use]
2212 pub fn new(mode: TrackerMode) -> Self {
2213 Self {
2214 inner: Arc::new(FakeTrackerInner {
2215 mode: Mutex::new(mode),
2216 per_destination: Mutex::new(HashMap::new()),
2217 filed: Mutex::new(Vec::new()),
2218 statused: Mutex::new(Vec::new()),
2219 commented: Mutex::new(Vec::new()),
2220 state: Mutex::new(TicketState::Open),
2221 }),
2222 }
2223 }
2224
2225 #[must_use]
2227 pub fn filed(&self) -> Vec<FiledCall> {
2228 self.inner.filed.lock().expect("tracker lock").clone()
2229 }
2230
2231 #[must_use]
2233 pub fn last_filed(&self) -> Option<FiledCall> {
2234 self.inner
2235 .filed
2236 .lock()
2237 .expect("tracker lock")
2238 .last()
2239 .cloned()
2240 }
2241
2242 #[must_use]
2244 pub fn statused(&self) -> Vec<StatusedCall> {
2245 self.inner.statused.lock().expect("tracker lock").clone()
2246 }
2247
2248 #[must_use]
2250 pub fn commented(&self) -> Vec<CommentedCall> {
2251 self.inner.commented.lock().expect("tracker lock").clone()
2252 }
2253
2254 pub fn set_mode(&self, mode: TrackerMode) {
2257 *self.inner.mode.lock().expect("tracker lock") = mode;
2258 }
2259
2260 pub fn set_mode_for(&self, dest: &Destination, mode: TrackerMode) {
2263 self.inner
2264 .per_destination
2265 .lock()
2266 .expect("tracker lock")
2267 .insert(dest.clone(), mode);
2268 }
2269
2270 pub fn clear_mode_for(&self, dest: &Destination) {
2273 self.inner
2274 .per_destination
2275 .lock()
2276 .expect("tracker lock")
2277 .remove(dest);
2278 }
2279
2280 #[must_use]
2282 pub fn mode_for(&self, dest: &Destination) -> TrackerMode {
2283 self.inner
2284 .per_destination
2285 .lock()
2286 .expect("tracker lock")
2287 .get(dest)
2288 .cloned()
2289 .unwrap_or_else(|| self.inner.mode.lock().expect("tracker lock").clone())
2290 }
2291
2292 pub fn set_state(&self, state: TicketState) {
2294 *self.inner.state.lock().expect("tracker lock") = state;
2295 }
2296
2297 #[must_use]
2299 pub fn state(&self) -> TicketState {
2300 *self.inner.state.lock().expect("tracker lock")
2301 }
2302
2303 fn error_for(mode: &TrackerMode) -> Option<TrackerError> {
2307 match mode {
2308 TrackerMode::FileOk => None,
2309 TrackerMode::NotConfigured => Some(TrackerError::NotConfigured),
2310 TrackerMode::Unauthorized => Some(TrackerError::Unauthorized),
2311 TrackerMode::Rejected => {
2312 Some(TrackerError::Rejected("fake tracker rejection".to_owned()))
2313 }
2314 TrackerMode::Transient => Some(TrackerError::Transient { retry_after: None }),
2315 TrackerMode::Error(error) => Some(error.clone()),
2316 }
2317 }
2318}
2319
2320impl Default for FakeTracker {
2321 fn default() -> Self {
2322 Self::new(TrackerMode::FileOk)
2323 }
2324}
2325
2326#[async_trait]
2327impl Tracker for FakeTracker {
2328 async fn file(
2329 &self,
2330 dest: &Destination,
2331 cred: &Credential,
2332 draft: &TicketDraft,
2333 ) -> Result<Filed, TrackerError> {
2334 if let Some(error) = Self::error_for(&self.mode_for(dest)) {
2335 return Err(error);
2336 }
2337 let id = format!(
2338 "fake-{}",
2339 self.inner.filed.lock().expect("tracker lock").len()
2340 );
2341 self.inner
2342 .filed
2343 .lock()
2344 .expect("tracker lock")
2345 .push(FiledCall {
2346 dest: dest.clone(),
2347 draft: draft.clone(),
2348 credential_fingerprint: fingerprint(cred),
2349 });
2350 Ok(Filed {
2351 url: format!("https://tracker.fake.test/browse/{id}"),
2352 external_id: id,
2353 })
2354 }
2355
2356 async fn status(
2357 &self,
2358 dest: &Destination,
2359 cred: &Credential,
2360 external_id: &str,
2361 ) -> Result<TicketStatus, TrackerError> {
2362 if let Some(error) = Self::error_for(&self.mode_for(dest)) {
2363 return Err(error);
2364 }
2365 self.inner
2366 .statused
2367 .lock()
2368 .expect("tracker lock")
2369 .push(StatusedCall {
2370 dest: dest.clone(),
2371 external_id: external_id.to_owned(),
2372 credential_fingerprint: fingerprint(cred),
2373 });
2374 Ok(TicketStatus {
2375 external_id: external_id.to_owned(),
2376 state: self.state(),
2377 url: None,
2378 })
2379 }
2380
2381 async fn comment(
2382 &self,
2383 dest: &Destination,
2384 cred: &Credential,
2385 external_id: &str,
2386 comment: &TicketComment,
2387 ) -> Result<(), TrackerError> {
2388 if let Some(error) = Self::error_for(&self.mode_for(dest)) {
2389 return Err(error);
2390 }
2391 self.inner
2392 .commented
2393 .lock()
2394 .expect("tracker lock")
2395 .push(CommentedCall {
2396 dest: dest.clone(),
2397 external_id: external_id.to_owned(),
2398 comment: comment.clone(),
2399 credential_fingerprint: fingerprint(cred),
2400 });
2401 Ok(())
2402 }
2403}
2404
2405type RealtimeMembers = std::collections::HashMap<String, Vec<String>>;
2410type RealtimeBroadcasts = Vec<(String, Vec<u8>)>;
2412
2413#[derive(Clone, Default)]
2419pub struct FakeRealtime {
2420 broadcasts: Arc<Mutex<RealtimeBroadcasts>>,
2421 members: Arc<Mutex<RealtimeMembers>>,
2422}
2423
2424impl FakeRealtime {
2425 #[must_use]
2426 pub fn new() -> Self {
2427 Self::default()
2428 }
2429
2430 pub fn set_members(&self, room_id: &str, ids: &[&str]) {
2432 self.members.lock().expect("realtime lock").insert(
2433 room_id.to_owned(),
2434 ids.iter().map(|id| (*id).to_owned()).collect(),
2435 );
2436 }
2437
2438 #[must_use]
2440 pub fn broadcasts(&self) -> Vec<(String, Vec<u8>)> {
2441 self.broadcasts.lock().expect("realtime lock").clone()
2442 }
2443}
2444
2445#[async_trait]
2446impl cratefield_core::Realtime for FakeRealtime {
2447 async fn broadcast(
2448 &self,
2449 room_id: &str,
2450 message: &[u8],
2451 ) -> Result<(), cratefield_core::RealtimeError> {
2452 self.broadcasts
2453 .lock()
2454 .expect("realtime lock")
2455 .push((room_id.to_owned(), message.to_vec()));
2456 Ok(())
2457 }
2458
2459 async fn members(
2460 &self,
2461 room_id: &str,
2462 ) -> Result<Vec<cratefield_core::Member>, cratefield_core::RealtimeError> {
2463 Ok(self
2464 .members
2465 .lock()
2466 .expect("realtime lock")
2467 .get(room_id)
2468 .map(|ids| ids.iter().map(cratefield_core::Member::new).collect())
2469 .unwrap_or_default())
2470 }
2471}
2472
2473#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2479pub enum AuthMode {
2480 Anonymous,
2483 TokenIsTheSubject,
2487 NotVerified,
2491 Unavailable,
2493}
2494
2495pub struct FakeAuth {
2498 mode: AuthMode,
2499 calls: std::sync::Mutex<Vec<String>>,
2500}
2501
2502impl FakeAuth {
2503 #[must_use]
2505 pub fn new(mode: AuthMode) -> Self {
2506 Self {
2507 mode,
2508 calls: std::sync::Mutex::new(Vec::new()),
2509 }
2510 }
2511
2512 #[must_use]
2514 pub fn subjects() -> Self {
2515 Self::new(AuthMode::TokenIsTheSubject)
2516 }
2517
2518 #[must_use]
2521 pub fn calls(&self) -> Vec<String> {
2522 self.calls.lock().expect("uncontended").clone()
2523 }
2524}
2525
2526#[async_trait::async_trait]
2527impl cratefield_core::Auth for FakeAuth {
2528 async fn identify(
2529 &self,
2530 headers: &http::HeaderMap,
2531 ) -> Result<cratefield_core::Caller, cratefield_core::AuthError> {
2532 let presented = headers
2533 .get(http::header::AUTHORIZATION)
2534 .and_then(|value| value.to_str().ok())
2535 .and_then(|value| value.strip_prefix("Bearer "))
2536 .map(str::to_owned);
2537 self.calls
2538 .lock()
2539 .expect("uncontended")
2540 .push(presented.clone().unwrap_or_default());
2541 match self.mode {
2542 AuthMode::Anonymous => Ok(cratefield_core::Caller::Anonymous),
2543 AuthMode::Unavailable => Err(cratefield_core::AuthError::Unavailable(
2544 "the fake is in Unavailable mode".to_owned(),
2545 )),
2546 AuthMode::NotVerified | AuthMode::TokenIsTheSubject => match presented {
2547 None => Ok(cratefield_core::Caller::Anonymous),
2548 Some(_) if self.mode == AuthMode::NotVerified => {
2549 Err(cratefield_core::AuthError::NotVerified)
2550 }
2551 Some(token) => Ok(cratefield_core::Caller::Subject(
2552 cratefield_core::Subject::new(token).session("fake-session"),
2553 )),
2554 },
2555 }
2556 }
2557}
2558
2559#[derive(Clone)]
2570pub struct FakeCustomHostnames {
2571 inner: Arc<FakeCustomHostnamesInner>,
2572}
2573
2574struct FakeCustomHostnamesInner {
2575 own_zone: String,
2576 hostnames: Mutex<BTreeMap<String, CustomHostname>>,
2577 calls: Mutex<Vec<String>>,
2578 refusals: Mutex<VecDeque<CustomHostnameError>>,
2580 next_id: AtomicUsize,
2581}
2582
2583impl FakeCustomHostnames {
2584 #[must_use]
2587 pub fn new(own_zone: impl Into<String>) -> Self {
2588 Self {
2589 inner: Arc::new(FakeCustomHostnamesInner {
2590 own_zone: own_zone.into(),
2591 hostnames: Mutex::new(BTreeMap::new()),
2592 calls: Mutex::new(Vec::new()),
2593 refusals: Mutex::new(VecDeque::new()),
2594 next_id: AtomicUsize::new(0),
2595 }),
2596 }
2597 }
2598
2599 #[must_use]
2601 pub fn calls(&self) -> Vec<String> {
2602 self.inner
2603 .calls
2604 .lock()
2605 .expect("custom hostnames lock")
2606 .clone()
2607 }
2608
2609 pub fn activate(&self, hostname: &str) {
2614 let key = self.key(hostname);
2615 if let Some(claim) = self
2616 .inner
2617 .hostnames
2618 .lock()
2619 .expect("custom hostnames lock")
2620 .get_mut(&key)
2621 {
2622 claim.status = ProviderStatus::Active;
2623 claim.certificate = CertificateStatus::Active;
2624 claim.validation.clear();
2625 }
2626 }
2627
2628 pub fn fail(&self, hostname: &str, reason: &str) {
2630 let key = self.key(hostname);
2631 if let Some(claim) = self
2632 .inner
2633 .hostnames
2634 .lock()
2635 .expect("custom hostnames lock")
2636 .get_mut(&key)
2637 {
2638 claim.status = ProviderStatus::Failed {
2639 reason: reason.to_owned(),
2640 };
2641 }
2642 }
2643
2644 pub fn refuse_next(&self, error: CustomHostnameError) {
2647 self.inner
2648 .refusals
2649 .lock()
2650 .expect("custom hostnames lock")
2651 .push_back(error);
2652 }
2653
2654 fn key(&self, hostname: &str) -> String {
2657 check_hostname(hostname, &self.inner.own_zone)
2658 .unwrap_or_else(|_| hostname.trim().trim_end_matches('.').to_ascii_lowercase())
2659 }
2660
2661 fn log(&self, call: String) {
2662 self.inner
2663 .calls
2664 .lock()
2665 .expect("custom hostnames lock")
2666 .push(call);
2667 }
2668
2669 fn take_refusal(&self) -> Option<CustomHostnameError> {
2670 self.inner
2671 .refusals
2672 .lock()
2673 .expect("custom hostnames lock")
2674 .pop_front()
2675 }
2676}
2677
2678#[async_trait]
2679impl CustomHostnames for FakeCustomHostnames {
2680 async fn create(&self, claim: &HostnameClaim) -> Result<CustomHostname, CustomHostnameError> {
2681 self.log(format!("create {}", claim.hostname));
2682 if let Some(error) = self.take_refusal() {
2683 return Err(error);
2684 }
2685 let hostname = check_hostname(&claim.hostname, &self.inner.own_zone)
2686 .map_err(CustomHostnameError::Refused)?;
2687 let mut hostnames = self.inner.hostnames.lock().expect("custom hostnames lock");
2688 if hostnames.contains_key(&hostname) {
2689 return Err(CustomHostnameError::AlreadyExists);
2690 }
2691 let id = format!(
2692 "fake-{}",
2693 self.inner.next_id.fetch_add(1, Ordering::Relaxed)
2694 );
2695 let created = CustomHostname {
2696 id: id.clone(),
2697 hostname: hostname.clone(),
2698 status: ProviderStatus::Pending,
2699 certificate: CertificateStatus::Pending,
2700 validation: vec![Validation {
2701 record_type: DnsRecordType::Txt,
2702 name: format!("_cf-custom-hostname.{hostname}"),
2703 value: id,
2704 }],
2705 };
2706 hostnames.insert(hostname, created.clone());
2707 Ok(created)
2708 }
2709
2710 async fn get(&self, hostname: &str) -> Result<Option<CustomHostname>, CustomHostnameError> {
2711 self.log(format!("get {hostname}"));
2712 if let Some(error) = self.take_refusal() {
2713 return Err(error);
2714 }
2715 let key = self.key(hostname);
2716 Ok(self
2717 .inner
2718 .hostnames
2719 .lock()
2720 .expect("custom hostnames lock")
2721 .get(&key)
2722 .cloned())
2723 }
2724
2725 async fn delete(&self, hostname: &str) -> Result<(), CustomHostnameError> {
2726 self.log(format!("delete {hostname}"));
2727 if let Some(error) = self.take_refusal() {
2728 return Err(error);
2729 }
2730 let key = self.key(hostname);
2731 self.inner
2732 .hostnames
2733 .lock()
2734 .expect("custom hostnames lock")
2735 .remove(&key);
2736 Ok(())
2737 }
2738
2739 async fn refresh(&self, hostname: &str) -> Result<CustomHostname, CustomHostnameError> {
2740 self.log(format!("refresh {hostname}"));
2741 if let Some(error) = self.take_refusal() {
2742 return Err(error);
2743 }
2744 let key = self.key(hostname);
2745 self.inner
2746 .hostnames
2747 .lock()
2748 .expect("custom hostnames lock")
2749 .get(&key)
2750 .cloned()
2751 .ok_or(CustomHostnameError::NotFound)
2752 }
2753}
2754
2755#[cfg(test)]
2756mod tests {
2757 use super::*;
2758 use cratefield_core::Blob as _;
2759
2760 fn json_body(response: &Response<Bytes>) -> serde_json::Value {
2761 serde_json::from_slice(response.body()).expect("the body is json")
2762 }
2763
2764 #[test]
2765 fn problem_json_carries_the_problem_fields_and_media_type() {
2766 let response = problem_json(
2767 StatusCode::TOO_MANY_REQUESTS,
2768 "Too Many Requests",
2769 "slow down",
2770 );
2771 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
2772 assert_eq!(response.headers()[CONTENT_TYPE], "application/problem+json");
2773 let body = json_body(&response);
2774 assert_eq!(body["type"], "about:blank");
2775 assert_eq!(body["title"], "Too Many Requests");
2776 assert_eq!(body["status"], 429);
2777 assert_eq!(body["detail"], "slow down");
2778 }
2779
2780 #[test]
2781 fn error_json_is_the_colonizer_shape() {
2782 let response = error_json(StatusCode::BAD_REQUEST, "bad request");
2783 assert_eq!(response.headers()[CONTENT_TYPE], "application/json");
2784 assert_eq!(json_body(&response)["error"], "bad request");
2785 }
2786
2787 #[test]
2788 fn with_retry_after_sets_the_header_in_seconds() {
2789 let response = with_retry_after(error_json(StatusCode::TOO_MANY_REQUESTS, "slow"), 42);
2790 assert_eq!(response.headers()[RETRY_AFTER], "42");
2791 }
2792
2793 #[test]
2794 fn a_preset_answers_through_the_port() {
2795 let http = FakeHttpClient::owlpost_rate_limited(7);
2798 let request = Request::builder()
2799 .uri("https://api.owlpost.fake/v1/emails")
2800 .body(Bytes::new())
2801 .expect("the request builds");
2802 let response = pollster::block_on(http.send(request)).expect("the preset answers");
2803 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
2804 assert_eq!(response.headers()[RETRY_AFTER], "7");
2805 assert_eq!(json_body(&response)["detail"], "slow down");
2806 }
2807
2808 #[pollster::test]
2809 async fn memory_blob_presigns_and_verifies_back() {
2810 let at = time::OffsetDateTime::from_unix_timestamp(1_577_836_800).expect("a valid epoch");
2813 let blob = MemoryBlob::with_presign(Arc::new(FixedClock(at)));
2814 let ttl = Duration::from_secs(3600);
2815
2816 let get = blob.signed_url("cms/clip.mp3", ttl).await.unwrap();
2817 assert!(get.starts_with("https://blob.test/cms/clip.mp3?"), "{get}");
2818 assert_eq!(
2819 blob.verify_presigned(&get, "GET", &[]).unwrap(),
2820 "cms/clip.mp3"
2821 );
2822
2823 let put = blob
2824 .signed_put_url("cms/clip.mp3", "audio/mpeg", Some(3), ttl)
2825 .await
2826 .unwrap();
2827 assert_eq!((put.method, put.headers.len()), ("PUT", 2));
2828 assert!(blob.verify_presigned(&put.url, "PUT", &put.headers).is_ok());
2829 assert!(blob.verify_presigned(&put.url, "PUT", &[]).is_err());
2831
2832 assert!(matches!(
2834 MemoryBlob::new().signed_url("k", ttl).await.unwrap_err(),
2835 cratefield_core::BlobError::Unsupported(_)
2836 ));
2837 }
2838}
2839
2840#[derive(Debug, Clone, Copy, Default)]
2849pub struct FakeUrlSafety;
2850
2851#[async_trait]
2852impl UrlSafety for FakeUrlSafety {
2853 async fn check(&self, url: &str) -> Result<UrlVerdict, UrlSafetyError> {
2854 check_url(url, &UrlSafetyLimits::default())?;
2855 Ok(UrlVerdict {
2856 url: url.to_owned(),
2857 threats: Vec::new(),
2858 source: UrlSafetySource::GoogleWebRisk,
2859 checked_at: time::OffsetDateTime::from_unix_timestamp(1_800_000_000)
2860 .expect("fixed epoch"),
2861 cache_ttl: None,
2862 cached: false,
2863 })
2864 }
2865}