#![allow(clippy::disallowed_types)]
#![allow(clippy::missing_panics_doc)]
use async_trait::async_trait;
use bytes::Bytes;
use cratefield_core::{
Captcha, CaptchaError, Clock, Database, DbError, Decision, Defer, HttpClient, HttpError,
KeyValue, KvError, MailError, Mailer, Message, RateLimitError, RateLimiter, Row, Rows,
SendOutcome, Statement, Verdict,
};
use futures_core::future::BoxFuture;
use http::{Request, Response};
use std::collections::{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, Copy, PartialEq, Eq)]
pub enum MailerMode {
SendOk,
NotConfigured,
Fail,
}
#[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");
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())),
}
}
}
#[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()),
})
}
}
#[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,
},
)
}
#[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(&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> {
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, Copy, PartialEq, Eq)]
pub enum PushMode {
DeliverOk,
NotConfigured,
Unregistered,
Transient,
}
#[derive(Clone)]
pub struct FakePush {
inner: Arc<FakePushInner>,
}
struct FakePushInner {
mode: Mutex<PushMode>,
sent: Mutex<Vec<(String, cratefield_core::Notification)>>,
}
impl FakePush {
#[must_use]
pub fn new(mode: PushMode) -> Self {
Self {
inner: Arc::new(FakePushInner {
mode: Mutex::new(mode),
sent: Mutex::new(Vec::new()),
}),
}
}
#[must_use]
pub fn sent(&self) -> Vec<(String, cratefield_core::Notification)> {
self.inner.sent.lock().expect("push lock").clone()
}
#[must_use]
pub fn last(&self) -> Option<(String, cratefield_core::Notification)> {
self.inner.sent.lock().expect("push lock").last().cloned()
}
pub fn set_mode(&self, mode: PushMode) {
*self.inner.mode.lock().expect("push lock") = mode;
}
}
impl Default for FakePush {
fn default() -> Self {
Self::new(PushMode::DeliverOk)
}
}
#[async_trait]
impl cratefield_core::Push for FakePush {
async fn send(
&self,
device_token: &str,
notification: &cratefield_core::Notification,
) -> Result<cratefield_core::PushOutcome, cratefield_core::PushError> {
let mode = *self.inner.mode.lock().expect("push lock");
match mode {
PushMode::DeliverOk => {
let id = format!(
"fake-apns-{}",
self.inner.sent.lock().expect("push lock").len()
);
self.inner
.sent
.lock()
.expect("push lock")
.push((device_token.to_owned(), 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".to_owned(),
)),
}
}
}
#[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" }),
})
}
}