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