#![allow(clippy::disallowed_types)]
#![allow(clippy::missing_panics_doc)]
use async_trait::async_trait;
use bytes::Bytes;
use cratefield_core::{
Answer, AnswerValue, Calibration, Captcha, CaptchaError, Classifier, ClassifierError,
ClassifierProfile, Clock, Completion, Credential, Database, DbError, Decision, Defer,
Destination, Filed, HttpClient, HttpError, KeyValue, KvError, MailError, Mailer, Message,
ModelTier, Prompt, Question, RateLimitError, RateLimiter, Row, Rows, SendOutcome, Statement,
TextModel, TextModelError, TicketDraft, TicketState, TicketStatus, Tracker, TrackerError,
Verdict, validate_questions,
};
use futures_core::future::BoxFuture;
use http::{Request, Response};
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
#[allow(clippy::disallowed_types)]
use std::sync::Mutex;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MailerMode {
SendOk,
NotConfigured,
Fail,
Error(MailError),
}
#[derive(Clone)]
pub struct FakeMailer {
inner: Arc<FakeMailerInner>,
}
struct FakeMailerInner {
mode: Mutex<MailerMode>,
sent: Mutex<Vec<Message>>,
}
impl FakeMailer {
#[must_use]
pub fn new(mode: MailerMode) -> Self {
Self {
inner: Arc::new(FakeMailerInner {
mode: Mutex::new(mode),
sent: Mutex::new(Vec::new()),
}),
}
}
#[must_use]
pub fn sent(&self) -> Vec<Message> {
self.inner.sent.lock().expect("mailer lock").clone()
}
#[must_use]
pub fn last_message(&self) -> Option<Message> {
self.inner.sent.lock().expect("mailer lock").last().cloned()
}
pub fn set_mode(&self, mode: MailerMode) {
*self.inner.mode.lock().expect("mailer lock") = mode;
}
}
#[async_trait]
impl Mailer for FakeMailer {
async fn send(&self, message: Message) -> Result<SendOutcome, MailError> {
let mode = self.inner.mode.lock().expect("mailer lock").clone();
match mode {
MailerMode::SendOk => {
let id = format!(
"fake-{}",
self.inner.sent.lock().expect("mailer lock").len()
);
self.inner.sent.lock().expect("mailer lock").push(message);
Ok(SendOutcome::Sent { id })
}
MailerMode::NotConfigured => Ok(SendOutcome::NotConfigured),
MailerMode::Fail => Err(MailError::Upstream("fake mailer failure".to_string())),
MailerMode::Error(error) => Err(error),
}
}
}
#[derive(Clone)]
pub struct FakeCaptcha {
allow_all: bool,
allowed_tokens: Arc<Vec<String>>,
}
impl FakeCaptcha {
#[must_use]
pub fn allow_all() -> Self {
Self {
allow_all: true,
allowed_tokens: Arc::new(Vec::new()),
}
}
#[must_use]
pub fn with_tokens(tokens: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
allow_all: false,
allowed_tokens: Arc::new(tokens.into_iter().map(Into::into).collect()),
}
}
}
#[async_trait]
impl Captcha for FakeCaptcha {
async fn verify(&self, token: &str, _remote_ip: Option<&str>) -> Result<Verdict, CaptchaError> {
let ok = self.allow_all || self.allowed_tokens.iter().any(|t| t == token);
Ok(Verdict {
ok,
reason: (!ok).then(|| "token not allowed".to_string()),
})
}
fn binding(&self) -> Option<cratefield_core::CaptchaBinding> {
Some(cratefield_core::CaptchaBinding {
hostname_bound: true,
action_bound: true,
fail_open: false,
})
}
}
#[derive(Clone)]
pub struct FakeRateLimiter {
inner: Arc<FakeRateLimiterInner>,
}
struct FakeRateLimiterInner {
scripted: Mutex<VecDeque<Decision>>,
default: Decision,
calls: AtomicUsize,
}
impl FakeRateLimiter {
#[must_use]
pub fn scripted(decisions: Vec<Decision>, default: Decision) -> Self {
Self {
inner: Arc::new(FakeRateLimiterInner {
scripted: Mutex::new(decisions.into_iter().collect()),
default,
calls: AtomicUsize::new(0),
}),
}
}
#[must_use]
pub fn always_allow() -> Self {
Self::scripted(
Vec::new(),
Decision {
ok: true,
retry_after: None,
quota: None,
},
)
}
#[must_use]
pub fn calls(&self) -> usize {
self.inner.calls.load(Ordering::SeqCst)
}
}
#[async_trait]
impl RateLimiter for FakeRateLimiter {
async fn limit(&self, _key: &str) -> Result<Decision, RateLimitError> {
self.inner.calls.fetch_add(1, Ordering::SeqCst);
let scripted = self
.inner
.scripted
.lock()
.expect("limiter lock")
.pop_front();
Ok(scripted.unwrap_or_else(|| self.inner.default.clone()))
}
}
#[derive(Debug, Clone)]
pub struct FixedClock(pub time::OffsetDateTime);
#[async_trait]
impl Clock for FixedClock {
fn now(&self) -> time::OffsetDateTime {
self.0
}
}
#[derive(Clone, Default)]
pub struct MemoryKeyValue {
inner: Arc<MemoryKeyValueInner>,
}
#[derive(Default)]
struct MemoryKeyValueInner {
entries: Mutex<HashMap<String, String>>,
}
impl MemoryKeyValue {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
#[async_trait]
impl KeyValue for MemoryKeyValue {
async fn get(&self, key: &str) -> Result<Option<String>, KvError> {
Ok(self
.inner
.entries
.lock()
.expect("kv lock")
.get(key)
.cloned())
}
async fn put(&self, key: &str, value: &str, _ttl: Option<Duration>) -> Result<(), KvError> {
self.inner
.entries
.lock()
.expect("kv lock")
.insert(key.to_string(), value.to_string());
Ok(())
}
async fn delete(&self, key: &str) -> Result<(), KvError> {
self.inner.entries.lock().expect("kv lock").remove(key);
Ok(())
}
}
#[derive(Clone)]
pub struct FakeHttpClient {
inner: Arc<FakeHttpInner>,
}
struct FakeHttpInner {
responses: Mutex<VecDeque<Result<Response<Bytes>, HttpError>>>,
captured: Mutex<Vec<(String, String, String)>>, }
impl FakeHttpClient {
#[must_use]
pub fn scripted(responses: Vec<Result<Response<Bytes>, HttpError>>) -> Self {
Self {
inner: Arc::new(FakeHttpInner {
responses: Mutex::new(responses.into_iter().collect()),
captured: Mutex::new(Vec::new()),
}),
}
}
#[must_use]
pub fn ok_json(body: &'static str) -> Self {
Self::scripted(vec![
Response::builder()
.status(200)
.body(Bytes::from(body))
.map_err(|err| HttpError::Transport(err.to_string())),
])
}
#[must_use]
pub fn captured(&self) -> Vec<(String, String, String)> {
self.inner.captured.lock().expect("http lock").clone()
}
}
#[async_trait]
impl HttpClient for FakeHttpClient {
async fn send(&self, request: Request<Bytes>) -> Result<Response<Bytes>, HttpError> {
let (parts, body) = request.into_parts();
self.inner.captured.lock().expect("http lock").push((
parts.method.to_string(),
parts.uri.to_string(),
String::from_utf8_lossy(&body).to_string(),
));
let next = self.inner.responses.lock().expect("http lock").pop_front();
next.unwrap_or_else(|| Err(HttpError::Transport("fake http exhausted".to_string())))
}
}
#[derive(Clone, Default)]
pub struct FakeDefer {
inner: Arc<FakeDeferInner>,
}
#[derive(Default)]
struct FakeDeferInner {
pending: Mutex<Vec<BoxFuture<'static, ()>>>,
deferred: AtomicUsize,
}
impl FakeDefer {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[allow(clippy::unused_async, clippy::unused_async_trait_impl)]
pub async fn drain(&self) {
while !self.inner.pending.lock().expect("defer lock").is_empty() {
let next = self.inner.pending.lock().expect("defer lock").remove(0);
pollster::block_on(next);
}
}
#[must_use]
pub fn deferred_count(&self) -> usize {
self.inner.deferred.load(Ordering::SeqCst)
}
}
impl Defer for FakeDefer {
fn wait_until(&self, fut: BoxFuture<'static, ()>) {
self.inner.deferred.fetch_add(1, Ordering::SeqCst);
self.inner.pending.lock().expect("defer lock").push(fut);
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct EmptyDatabase;
#[async_trait]
impl Database for EmptyDatabase {
async fn execute(&self, stmt: &Statement) -> Result<u64, DbError> {
Err(DbError::Execute(format!("empty database: {}", stmt.sql)))
}
async fn query(&self, stmt: &Statement) -> Result<Rows, DbError> {
if stmt.sql.trim() == "SELECT 1" {
Ok(Rows::new(vec![Row::new(vec![(
"1".to_string(),
sea_query::Value::Int(Some(1)),
)])]))
} else {
Err(DbError::Query(format!("empty database: {}", stmt.sql)))
}
}
async fn batch_atomic(&self, _stmts: &[Statement]) -> Result<(), DbError> {
Err(DbError::Batch("empty database".to_string()))
}
}
#[derive(Clone)]
pub struct FakeDispatcher {
binding: String,
behaviour: FakeDispatch,
calls: Arc<AtomicUsize>,
}
#[derive(Clone)]
enum FakeDispatch {
Serve(Arc<Mutex<axum::Router>>),
Unbound,
Failing(String),
}
impl FakeDispatcher {
#[must_use]
pub fn serving(binding: impl Into<String>, router: axum::Router) -> Self {
Self {
binding: binding.into(),
behaviour: FakeDispatch::Serve(Arc::new(Mutex::new(router))),
calls: Arc::new(AtomicUsize::new(0)),
}
}
#[must_use]
pub fn unbound() -> Self {
Self {
binding: String::new(),
behaviour: FakeDispatch::Unbound,
calls: Arc::new(AtomicUsize::new(0)),
}
}
#[must_use]
pub fn failing(binding: impl Into<String>, reason: impl Into<String>) -> Self {
Self {
binding: binding.into(),
behaviour: FakeDispatch::Failing(reason.into()),
calls: Arc::new(AtomicUsize::new(0)),
}
}
#[must_use]
pub fn calls(&self) -> usize {
self.calls.load(Ordering::SeqCst)
}
}
#[async_trait]
impl cratefield_core::Dispatcher for FakeDispatcher {
fn has(&self, binding: &str) -> bool {
!matches!(self.behaviour, FakeDispatch::Unbound) && binding == self.binding
}
async fn dispatch(
&self,
binding: &str,
request: Request<Bytes>,
) -> Result<Response<Bytes>, cratefield_core::DispatchError> {
self.calls.fetch_add(1, Ordering::SeqCst);
match &self.behaviour {
FakeDispatch::Unbound => {
Err(cratefield_core::DispatchError::NotBound(binding.to_owned()))
}
FakeDispatch::Failing(reason) => Err(cratefield_core::DispatchError::Unavailable {
binding: binding.to_owned(),
reason: reason.clone(),
}),
FakeDispatch::Serve(router) => {
let router = router.lock().unwrap().clone();
let (parts, body) = request.into_parts();
let request = Request::from_parts(parts, axum::body::Body::from(body));
let response =
tower::ServiceExt::oneshot(router, request)
.await
.map_err(|err| cratefield_core::DispatchError::Unavailable {
binding: binding.to_owned(),
reason: err.to_string(),
})?;
let (parts, body) = response.into_parts();
let bytes = axum::body::to_bytes(body, usize::MAX)
.await
.map_err(|err| cratefield_core::DispatchError::Unavailable {
binding: binding.to_owned(),
reason: err.to_string(),
})?;
Ok(Response::from_parts(parts, bytes))
}
}
}
}
#[derive(Clone, Default)]
pub struct MemoryBlob {
objects: Arc<std::sync::Mutex<std::collections::HashMap<String, cratefield_core::BlobObject>>>,
}
impl MemoryBlob {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn len(&self) -> usize {
self.objects.lock().unwrap().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[async_trait]
impl cratefield_core::Blob for MemoryBlob {
async fn put(
&self,
key: &str,
bytes: &[u8],
content_type: &str,
) -> Result<(), cratefield_core::BlobError> {
cratefield_core::check_blob_size(bytes)?;
self.objects.lock().unwrap().insert(
key.to_owned(),
cratefield_core::BlobObject {
bytes: bytes.to_vec(),
content_type: content_type.to_owned(),
},
);
Ok(())
}
async fn get(
&self,
key: &str,
) -> Result<Option<cratefield_core::BlobObject>, cratefield_core::BlobError> {
Ok(self.objects.lock().unwrap().get(key).cloned())
}
async fn delete(&self, key: &str) -> Result<(), cratefield_core::BlobError> {
self.objects.lock().unwrap().remove(key);
Ok(())
}
async fn signed_url(
&self,
_key: &str,
_ttl: std::time::Duration,
) -> Result<String, cratefield_core::BlobError> {
Err(cratefield_core::BlobError::Unsupported(
"in-memory store has no presigned URLs".to_owned(),
))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PushMode {
DeliverOk,
NotConfigured,
Unregistered,
Transient,
Error(cratefield_core::PushError),
}
#[derive(Clone)]
pub struct FakePush {
inner: Arc<FakePushInner>,
}
struct FakePushInner {
mode: Mutex<PushMode>,
per_recipient: Mutex<HashMap<cratefield_core::Recipient, PushMode>>,
sent: Mutex<Vec<(cratefield_core::Recipient, cratefield_core::Notification)>>,
}
impl FakePush {
#[must_use]
pub fn new(mode: PushMode) -> Self {
Self {
inner: Arc::new(FakePushInner {
mode: Mutex::new(mode),
per_recipient: Mutex::new(HashMap::new()),
sent: Mutex::new(Vec::new()),
}),
}
}
#[must_use]
pub fn sent(&self) -> Vec<(cratefield_core::Recipient, cratefield_core::Notification)> {
self.inner.sent.lock().expect("push lock").clone()
}
#[must_use]
pub fn last(&self) -> Option<(cratefield_core::Recipient, cratefield_core::Notification)> {
self.inner.sent.lock().expect("push lock").last().cloned()
}
#[must_use]
pub fn sent_to(
&self,
recipient: &cratefield_core::Recipient,
) -> Vec<cratefield_core::Notification> {
self.inner
.sent
.lock()
.expect("push lock")
.iter()
.filter(|(to, _)| to == recipient)
.map(|(_, notification)| notification.clone())
.collect()
}
pub fn set_mode(&self, mode: PushMode) {
*self.inner.mode.lock().expect("push lock") = mode;
}
pub fn set_mode_for(&self, recipient: &cratefield_core::Recipient, mode: PushMode) {
self.inner
.per_recipient
.lock()
.expect("push lock")
.insert(recipient.clone(), mode);
}
pub fn clear_mode_for(&self, recipient: &cratefield_core::Recipient) {
self.inner
.per_recipient
.lock()
.expect("push lock")
.remove(recipient);
}
#[must_use]
pub fn mode_for(&self, recipient: &cratefield_core::Recipient) -> PushMode {
self.inner
.per_recipient
.lock()
.expect("push lock")
.get(recipient)
.cloned()
.unwrap_or_else(|| self.inner.mode.lock().expect("push lock").clone())
}
}
impl Default for FakePush {
fn default() -> Self {
Self::new(PushMode::DeliverOk)
}
}
#[async_trait]
impl cratefield_core::Push for FakePush {
async fn send(
&self,
to: &cratefield_core::Recipient,
notification: &cratefield_core::Notification,
) -> Result<cratefield_core::PushOutcome, cratefield_core::PushError> {
match self.mode_for(to) {
PushMode::DeliverOk => {
let id = format!(
"fake-push-{}",
self.inner.sent.lock().expect("push lock").len()
);
self.inner
.sent
.lock()
.expect("push lock")
.push((to.clone(), notification.clone()));
Ok(cratefield_core::PushOutcome::Delivered { id: Some(id) })
}
PushMode::NotConfigured => Ok(cratefield_core::PushOutcome::NotConfigured),
PushMode::Unregistered => Err(cratefield_core::PushError::Unregistered),
PushMode::Transient => Err(cratefield_core::PushError::transient("fake push failure")),
PushMode::Error(error) => Err(error),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TextModelMode {
Reply(String),
Complete(cratefield_core::Completion),
NotConfigured,
Rejected(String),
Transient { retry_after: Option<Duration> },
Error(cratefield_core::TextModelError),
}
#[derive(Clone)]
pub struct FakeTextModel {
inner: Arc<FakeTextModelInner>,
}
struct FakeTextModelInner {
mode: Mutex<TextModelMode>,
per_tier: Mutex<HashMap<ModelTier, TextModelMode>>,
prompts: Mutex<Vec<Prompt>>,
}
fn fake_usage(prompt: &Prompt, text: &str) -> (u64, u64) {
let prompt_chars = prompt.system.as_deref().map_or(0, str::len)
+ prompt
.messages
.iter()
.map(|turn| turn.content.len())
.sum::<usize>();
let input = (prompt_chars / 4).max(1) as u64;
let output = (text.len() / 4).max(1) as u64;
(input, output)
}
impl FakeTextModel {
#[must_use]
pub fn new(mode: TextModelMode) -> Self {
Self {
inner: Arc::new(FakeTextModelInner {
mode: Mutex::new(mode),
per_tier: Mutex::new(HashMap::new()),
prompts: Mutex::new(Vec::new()),
}),
}
}
#[must_use]
pub fn prompts(&self) -> Vec<Prompt> {
self.inner.prompts.lock().expect("text model lock").clone()
}
#[must_use]
pub fn last(&self) -> Option<Prompt> {
self.inner
.prompts
.lock()
.expect("text model lock")
.last()
.cloned()
}
pub fn set_mode(&self, mode: TextModelMode) {
*self.inner.mode.lock().expect("text model lock") = mode;
}
pub fn set_mode_for(&self, tier: ModelTier, mode: TextModelMode) {
self.inner
.per_tier
.lock()
.expect("text model lock")
.insert(tier, mode);
}
pub fn clear_mode_for(&self, tier: ModelTier) {
self.inner
.per_tier
.lock()
.expect("text model lock")
.remove(&tier);
}
#[must_use]
pub fn mode_for(&self, tier: ModelTier) -> TextModelMode {
self.inner
.per_tier
.lock()
.expect("text model lock")
.get(&tier)
.cloned()
.unwrap_or_else(|| self.inner.mode.lock().expect("text model lock").clone())
}
fn record(&self, prompt: &Prompt) {
self.inner
.prompts
.lock()
.expect("text model lock")
.push(prompt.clone());
}
}
impl Default for FakeTextModel {
fn default() -> Self {
Self::new(TextModelMode::Reply("fake completion".to_owned()))
}
}
#[async_trait]
impl TextModel for FakeTextModel {
async fn complete(&self, prompt: &Prompt) -> Result<Completion, TextModelError> {
match self.mode_for(prompt.tier) {
TextModelMode::Reply(text) => {
self.record(prompt);
let (input_tokens, output_tokens) = fake_usage(prompt, &text);
Ok(
Completion::new(text, format!("fake-{}", prompt.tier.name()))
.usage(input_tokens, output_tokens),
)
}
TextModelMode::Complete(completion) => {
self.record(prompt);
Ok(completion)
}
TextModelMode::NotConfigured => Err(TextModelError::NotConfigured),
TextModelMode::Rejected(message) => Err(TextModelError::Rejected(message)),
TextModelMode::Transient { retry_after } => {
Err(TextModelError::Transient { retry_after })
}
TextModelMode::Error(error) => Err(error),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ClassifierMode {
Answers(BTreeMap<String, Answer>),
Deterministic,
NotConfigured,
Rejected(String),
Transient { retry_after: Option<Duration> },
Error(ClassifierError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ask {
pub state: String,
pub question_ids: Vec<String>,
}
#[derive(Clone)]
pub struct FakeClassifier {
inner: Arc<FakeClassifierInner>,
}
struct FakeClassifierInner {
mode: Mutex<ClassifierMode>,
per_question: Mutex<BTreeMap<String, Answer>>,
asks: Mutex<Vec<Ask>>,
profile: Mutex<ClassifierProfile>,
}
fn deterministic_answer(question: &Question) -> Answer {
let labels = question.labels();
let chosen = labels.first().copied().unwrap_or_default();
let share = if labels.len() > 1 {
0.4 / f32::from(u16::try_from(labels.len() - 1).unwrap_or(1))
} else {
0.0
};
let probabilities: BTreeMap<String, f32> = labels
.iter()
.map(|label| {
let mass = if *label == chosen { 0.6 } else { share };
((*label).to_owned(), mass)
})
.collect();
match question {
Question::Choice { .. } => Answer::choice(chosen, probabilities),
Question::Noul { .. } => Answer::noul(chosen == "true", probabilities),
Question::Score { .. } => Answer::new(
AnswerValue::Score(chosen.parse().unwrap_or_default()),
probabilities,
0.6,
),
}
}
impl FakeClassifier {
#[must_use]
pub fn new(mode: ClassifierMode) -> Self {
Self {
inner: Arc::new(FakeClassifierInner {
mode: Mutex::new(mode),
per_question: Mutex::new(BTreeMap::new()),
asks: Mutex::new(Vec::new()),
profile: Mutex::new(ClassifierProfile::new(
Calibration::Classifier,
cratefield_core::DEFAULT_MAX_STATE_CHARS,
)),
}),
}
}
#[must_use]
pub fn asks(&self) -> Vec<Ask> {
self.inner.asks.lock().expect("classifier lock").clone()
}
#[must_use]
pub fn last(&self) -> Option<Ask> {
self.inner
.asks
.lock()
.expect("classifier lock")
.last()
.cloned()
}
pub fn set_mode(&self, mode: ClassifierMode) {
*self.inner.mode.lock().expect("classifier lock") = mode;
}
pub fn set_answer_for(&self, question_id: impl Into<String>, answer: Answer) {
self.inner
.per_question
.lock()
.expect("classifier lock")
.insert(question_id.into(), answer);
}
pub fn clear_answer_for(&self, question_id: &str) {
self.inner
.per_question
.lock()
.expect("classifier lock")
.remove(question_id);
}
pub fn set_profile(&self, calibration: Calibration, max_state_chars: usize) {
*self.inner.profile.lock().expect("classifier lock") =
ClassifierProfile::new(calibration, max_state_chars);
}
}
impl Default for FakeClassifier {
fn default() -> Self {
Self::new(ClassifierMode::Deterministic)
}
}
#[async_trait]
impl Classifier for FakeClassifier {
fn profile(&self) -> ClassifierProfile {
*self.inner.profile.lock().expect("classifier lock")
}
async fn ask(
&self,
state: &str,
questions: &BTreeMap<String, Question>,
) -> Result<BTreeMap<String, Answer>, ClassifierError> {
validate_questions(questions)?;
let mode = self.inner.mode.lock().expect("classifier lock").clone();
match mode {
ClassifierMode::NotConfigured => return Err(ClassifierError::NotConfigured),
ClassifierMode::Rejected(message) => return Err(ClassifierError::Rejected(message)),
ClassifierMode::Transient { retry_after } => {
return Err(ClassifierError::Transient { retry_after });
}
ClassifierMode::Error(error) => return Err(error),
ClassifierMode::Answers(_) | ClassifierMode::Deterministic => {}
}
let overrides = self.inner.per_question.lock().expect("classifier lock");
let answers: BTreeMap<String, Answer> = questions
.iter()
.map(|(id, question)| {
let answer = overrides.get(id).cloned().unwrap_or_else(|| match &mode {
ClassifierMode::Answers(map) => map
.get(id)
.cloned()
.unwrap_or_else(|| deterministic_answer(question)),
_ => deterministic_answer(question),
});
(id.clone(), answer)
})
.collect();
drop(overrides);
self.inner.asks.lock().expect("classifier lock").push(Ask {
state: state.to_owned(),
question_ids: questions.keys().cloned().collect(),
});
Ok(answers)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaymentsMode {
Ok,
NotConfigured,
Transient,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PaymentsCall {
Checkout,
SubscriptionCheckout,
ConnectAccountLink,
ChargeWithTransfer,
Refund,
VerifyWebhook,
}
#[derive(Clone)]
pub struct FakePayments {
inner: Arc<FakePaymentsInner>,
}
struct FakePaymentsInner {
mode: Mutex<PaymentsMode>,
calls: Mutex<Vec<PaymentsCall>>,
}
impl FakePayments {
#[must_use]
pub fn new(mode: PaymentsMode) -> Self {
Self {
inner: Arc::new(FakePaymentsInner {
mode: Mutex::new(mode),
calls: Mutex::new(Vec::new()),
}),
}
}
#[must_use]
pub fn calls(&self) -> Vec<PaymentsCall> {
self.inner.calls.lock().expect("payments lock").clone()
}
pub fn set_mode(&self, mode: PaymentsMode) {
*self.inner.mode.lock().expect("payments lock") = mode;
}
fn record(&self, call: PaymentsCall) {
self.inner.calls.lock().expect("payments lock").push(call);
}
fn guard(&self) -> Result<(), cratefield_core::PaymentsError> {
match *self.inner.mode.lock().expect("payments lock") {
PaymentsMode::Ok => Ok(()),
PaymentsMode::NotConfigured => Err(cratefield_core::PaymentsError::NotConfigured),
PaymentsMode::Transient => Err(cratefield_core::PaymentsError::Transient(
"fake payments failure".to_owned(),
)),
}
}
}
impl Default for FakePayments {
fn default() -> Self {
Self::new(PaymentsMode::Ok)
}
}
#[async_trait]
impl cratefield_core::Payments for FakePayments {
async fn create_checkout(
&self,
_request: &cratefield_core::CheckoutRequest,
) -> Result<cratefield_core::CheckoutSession, cratefield_core::PaymentsError> {
self.guard()?;
self.record(PaymentsCall::Checkout);
Ok(cratefield_core::CheckoutSession {
id: "cs_fake".to_owned(),
url: "https://checkout.stripe.test/cs_fake".to_owned(),
})
}
async fn create_subscription_checkout(
&self,
_request: &cratefield_core::SubscriptionCheckoutRequest,
) -> Result<cratefield_core::CheckoutSession, cratefield_core::PaymentsError> {
self.guard()?;
self.record(PaymentsCall::SubscriptionCheckout);
Ok(cratefield_core::CheckoutSession {
id: "cs_sub_fake".to_owned(),
url: "https://checkout.stripe.test/cs_sub_fake".to_owned(),
})
}
async fn create_connect_account_link(
&self,
_request: &cratefield_core::ConnectAccountLinkRequest,
) -> Result<cratefield_core::ConnectAccountLink, cratefield_core::PaymentsError> {
self.guard()?;
self.record(PaymentsCall::ConnectAccountLink);
Ok(cratefield_core::ConnectAccountLink {
account_id: "acct_fake".to_owned(),
url: "https://connect.stripe.test/acct_fake".to_owned(),
})
}
async fn charge_with_transfer(
&self,
_request: &cratefield_core::TransferCharge,
) -> Result<cratefield_core::Charge, cratefield_core::PaymentsError> {
self.guard()?;
self.record(PaymentsCall::ChargeWithTransfer);
Ok(cratefield_core::Charge {
id: "pi_fake".to_owned(),
status: "succeeded".to_owned(),
})
}
async fn refund(
&self,
_request: &cratefield_core::RefundRequest,
) -> Result<cratefield_core::Refund, cratefield_core::PaymentsError> {
self.guard()?;
self.record(PaymentsCall::Refund);
Ok(cratefield_core::Refund {
id: "re_fake".to_owned(),
})
}
async fn verify_webhook(
&self,
signature_header: &str,
_body: &[u8],
) -> Result<cratefield_core::WebhookEvent, cratefield_core::PaymentsError> {
self.record(PaymentsCall::VerifyWebhook);
if signature_header == "invalid" {
return Err(cratefield_core::PaymentsError::SignatureInvalid(
"fake tampered signature".to_owned(),
));
}
self.guard()?;
Ok(cratefield_core::WebhookEvent {
id: "evt_fake".to_owned(),
kind: "checkout.session.completed".to_owned(),
data: serde_json::json!({ "object": "checkout.session" }),
})
}
}
fn fingerprint(credential: &Credential) -> String {
use std::hash::{Hash, Hasher};
let mut hasher = std::hash::DefaultHasher::new();
credential.expose().hash(&mut hasher);
format!("fp:{:016x}", hasher.finish())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TrackerMode {
FileOk,
NotConfigured,
Unauthorized,
Rejected,
Transient,
Error(TrackerError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FiledCall {
pub dest: Destination,
pub draft: TicketDraft,
pub credential_fingerprint: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatusedCall {
pub dest: Destination,
pub external_id: String,
pub credential_fingerprint: String,
}
#[derive(Clone, Debug)]
pub struct FakeTracker {
inner: Arc<FakeTrackerInner>,
}
#[derive(Debug)]
struct FakeTrackerInner {
mode: Mutex<TrackerMode>,
per_destination: Mutex<HashMap<Destination, TrackerMode>>,
filed: Mutex<Vec<FiledCall>>,
statused: Mutex<Vec<StatusedCall>>,
state: Mutex<TicketState>,
}
impl FakeTracker {
#[must_use]
pub fn new(mode: TrackerMode) -> Self {
Self {
inner: Arc::new(FakeTrackerInner {
mode: Mutex::new(mode),
per_destination: Mutex::new(HashMap::new()),
filed: Mutex::new(Vec::new()),
statused: Mutex::new(Vec::new()),
state: Mutex::new(TicketState::Open),
}),
}
}
#[must_use]
pub fn filed(&self) -> Vec<FiledCall> {
self.inner.filed.lock().expect("tracker lock").clone()
}
#[must_use]
pub fn last_filed(&self) -> Option<FiledCall> {
self.inner
.filed
.lock()
.expect("tracker lock")
.last()
.cloned()
}
#[must_use]
pub fn statused(&self) -> Vec<StatusedCall> {
self.inner.statused.lock().expect("tracker lock").clone()
}
pub fn set_mode(&self, mode: TrackerMode) {
*self.inner.mode.lock().expect("tracker lock") = mode;
}
pub fn set_mode_for(&self, dest: &Destination, mode: TrackerMode) {
self.inner
.per_destination
.lock()
.expect("tracker lock")
.insert(dest.clone(), mode);
}
pub fn clear_mode_for(&self, dest: &Destination) {
self.inner
.per_destination
.lock()
.expect("tracker lock")
.remove(dest);
}
#[must_use]
pub fn mode_for(&self, dest: &Destination) -> TrackerMode {
self.inner
.per_destination
.lock()
.expect("tracker lock")
.get(dest)
.cloned()
.unwrap_or_else(|| self.inner.mode.lock().expect("tracker lock").clone())
}
pub fn set_state(&self, state: TicketState) {
*self.inner.state.lock().expect("tracker lock") = state;
}
#[must_use]
pub fn state(&self) -> TicketState {
*self.inner.state.lock().expect("tracker lock")
}
fn error_for(mode: &TrackerMode) -> Option<TrackerError> {
match mode {
TrackerMode::FileOk => None,
TrackerMode::NotConfigured => Some(TrackerError::NotConfigured),
TrackerMode::Unauthorized => Some(TrackerError::Unauthorized),
TrackerMode::Rejected => {
Some(TrackerError::Rejected("fake tracker rejection".to_owned()))
}
TrackerMode::Transient => Some(TrackerError::Transient { retry_after: None }),
TrackerMode::Error(error) => Some(error.clone()),
}
}
}
impl Default for FakeTracker {
fn default() -> Self {
Self::new(TrackerMode::FileOk)
}
}
#[async_trait]
impl Tracker for FakeTracker {
async fn file(
&self,
dest: &Destination,
cred: &Credential,
draft: &TicketDraft,
) -> Result<Filed, TrackerError> {
if let Some(error) = Self::error_for(&self.mode_for(dest)) {
return Err(error);
}
let id = format!(
"fake-{}",
self.inner.filed.lock().expect("tracker lock").len()
);
self.inner
.filed
.lock()
.expect("tracker lock")
.push(FiledCall {
dest: dest.clone(),
draft: draft.clone(),
credential_fingerprint: fingerprint(cred),
});
Ok(Filed {
url: format!("https://tracker.fake.test/browse/{id}"),
external_id: id,
})
}
async fn status(
&self,
dest: &Destination,
cred: &Credential,
external_id: &str,
) -> Result<TicketStatus, TrackerError> {
if let Some(error) = Self::error_for(&self.mode_for(dest)) {
return Err(error);
}
self.inner
.statused
.lock()
.expect("tracker lock")
.push(StatusedCall {
dest: dest.clone(),
external_id: external_id.to_owned(),
credential_fingerprint: fingerprint(cred),
});
Ok(TicketStatus {
external_id: external_id.to_owned(),
state: self.state(),
url: None,
})
}
}
type RealtimeMembers = std::collections::HashMap<String, Vec<String>>;
type RealtimeBroadcasts = Vec<(String, Vec<u8>)>;
#[derive(Clone, Default)]
pub struct FakeRealtime {
broadcasts: Arc<Mutex<RealtimeBroadcasts>>,
members: Arc<Mutex<RealtimeMembers>>,
}
impl FakeRealtime {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set_members(&self, room_id: &str, ids: &[&str]) {
self.members.lock().expect("realtime lock").insert(
room_id.to_owned(),
ids.iter().map(|id| (*id).to_owned()).collect(),
);
}
#[must_use]
pub fn broadcasts(&self) -> Vec<(String, Vec<u8>)> {
self.broadcasts.lock().expect("realtime lock").clone()
}
}
#[async_trait]
impl cratefield_core::Realtime for FakeRealtime {
async fn broadcast(
&self,
room_id: &str,
message: &[u8],
) -> Result<(), cratefield_core::RealtimeError> {
self.broadcasts
.lock()
.expect("realtime lock")
.push((room_id.to_owned(), message.to_vec()));
Ok(())
}
async fn members(
&self,
room_id: &str,
) -> Result<Vec<cratefield_core::Member>, cratefield_core::RealtimeError> {
Ok(self
.members
.lock()
.expect("realtime lock")
.get(room_id)
.map(|ids| ids.iter().map(cratefield_core::Member::new).collect())
.unwrap_or_default())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthMode {
Anonymous,
TokenIsTheSubject,
NotVerified,
Unavailable,
}
pub struct FakeAuth {
mode: AuthMode,
calls: std::sync::Mutex<Vec<String>>,
}
impl FakeAuth {
#[must_use]
pub fn new(mode: AuthMode) -> Self {
Self {
mode,
calls: std::sync::Mutex::new(Vec::new()),
}
}
#[must_use]
pub fn subjects() -> Self {
Self::new(AuthMode::TokenIsTheSubject)
}
#[must_use]
pub fn calls(&self) -> Vec<String> {
self.calls.lock().expect("uncontended").clone()
}
}
#[async_trait::async_trait]
impl cratefield_core::Auth for FakeAuth {
async fn identify(
&self,
headers: &http::HeaderMap,
) -> Result<cratefield_core::Caller, cratefield_core::AuthError> {
let presented = headers
.get(http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.map(str::to_owned);
self.calls
.lock()
.expect("uncontended")
.push(presented.clone().unwrap_or_default());
match self.mode {
AuthMode::Anonymous => Ok(cratefield_core::Caller::Anonymous),
AuthMode::Unavailable => Err(cratefield_core::AuthError::Unavailable(
"the fake is in Unavailable mode".to_owned(),
)),
AuthMode::NotVerified | AuthMode::TokenIsTheSubject => match presented {
None => Ok(cratefield_core::Caller::Anonymous),
Some(_) if self.mode == AuthMode::NotVerified => {
Err(cratefield_core::AuthError::NotVerified)
}
Some(token) => Ok(cratefield_core::Caller::Subject(
cratefield_core::Subject::new(token).session("fake-session"),
)),
},
}
}
}