use crate::config::settings::Settings;
use mostro_core::prelude::Message;
use nostr_sdk::{Client, Keys, PublicKey};
use sqlx::{Pool, Sqlite};
use std::sync::Arc;
use tokio::sync::RwLock;
pub type OrderMsgQueue = Arc<RwLock<Vec<(Message, PublicKey)>>>;
#[derive(Clone)]
pub struct AppContext {
pool: Arc<Pool<Sqlite>>,
nostr_client: Client,
settings: Arc<Settings>,
order_msg_queue: OrderMsgQueue,
keys: Keys,
}
impl AppContext {
pub fn new(
pool: Arc<Pool<Sqlite>>,
nostr_client: Client,
settings: Arc<Settings>,
order_msg_queue: OrderMsgQueue,
keys: Keys,
) -> Self {
Self {
pool,
nostr_client,
settings,
order_msg_queue,
keys,
}
}
pub fn pool(&self) -> &Pool<Sqlite> {
&self.pool
}
pub fn pool_arc(&self) -> Arc<Pool<Sqlite>> {
self.pool.clone()
}
pub fn nostr_client(&self) -> &Client {
&self.nostr_client
}
pub fn settings(&self) -> &Settings {
&self.settings
}
pub fn order_msg_queue(&self) -> &OrderMsgQueue {
&self.order_msg_queue
}
pub fn keys(&self) -> &Keys {
&self.keys
}
}
#[cfg(test)]
pub mod test_utils {
use super::*;
use crate::config::types::{
DatabaseSettings, ExpirationSettings, LightningSettings, MostroSettings, NostrSettings,
RpcSettings,
};
#[derive(Debug, Clone)]
pub struct MockOrderMsgQueue {
queue: OrderMsgQueue,
}
impl MockOrderMsgQueue {
pub fn new() -> Self {
Self {
queue: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn queue(&self) -> OrderMsgQueue {
self.queue.clone()
}
pub async fn len(&self) -> usize {
self.queue.read().await.len()
}
pub async fn is_empty(&self) -> bool {
self.queue.read().await.is_empty()
}
pub async fn clear(&self) {
self.queue.write().await.clear();
}
}
impl Default for MockOrderMsgQueue {
fn default() -> Self {
Self::new()
}
}
pub struct TestContextBuilder {
pool: Option<Arc<Pool<Sqlite>>>,
nostr_client: Option<Client>,
settings: Option<Arc<Settings>>,
order_msg_queue: Option<OrderMsgQueue>,
mock_order_msg_queue: Option<MockOrderMsgQueue>,
keys: Option<Keys>,
}
impl TestContextBuilder {
pub fn new() -> Self {
Self {
pool: None,
nostr_client: None,
settings: None,
order_msg_queue: None,
mock_order_msg_queue: None,
keys: None,
}
}
pub fn with_pool(mut self, pool: Arc<Pool<Sqlite>>) -> Self {
self.pool = Some(pool);
self
}
pub fn with_nostr_client(mut self, client: Client) -> Self {
self.nostr_client = Some(client);
self
}
pub fn with_settings(mut self, settings: Settings) -> Self {
self.settings = Some(Arc::new(settings));
self
}
pub fn with_order_msg_queue(mut self, queue: OrderMsgQueue) -> Self {
self.order_msg_queue = Some(queue);
self.mock_order_msg_queue = None;
self
}
pub fn with_mock_order_msg_queue(mut self, mock: MockOrderMsgQueue) -> Self {
self.order_msg_queue = Some(mock.queue());
self.mock_order_msg_queue = Some(mock);
self
}
pub fn with_keys(mut self, keys: Keys) -> Self {
self.keys = Some(keys);
self
}
pub fn build(self) -> AppContext {
let pool = self
.pool
.expect("TestContextBuilder requires with_pool() for synchronous build");
let nostr_client = self.nostr_client.unwrap_or_default();
let settings = self
.settings
.expect("TestContextBuilder requires with_settings() — Settings has no Default");
let order_msg_queue = self
.order_msg_queue
.unwrap_or_else(|| Arc::new(RwLock::new(Vec::new())));
let keys = self.keys.unwrap_or_else(|| {
Keys::parse(&settings.nostr.nsec_privkey)
.expect("TestContextBuilder: invalid nsec_privkey in settings")
});
AppContext::new(pool, nostr_client, settings, order_msg_queue, keys)
}
pub fn build_with_mocks(self) -> (AppContext, Option<MockOrderMsgQueue>) {
let mock_order_msg_queue = self.mock_order_msg_queue.clone();
let ctx = self.build();
(ctx, mock_order_msg_queue)
}
}
impl Default for TestContextBuilder {
fn default() -> Self {
Self::new()
}
}
pub fn test_settings() -> Settings {
Settings {
database: DatabaseSettings {
url: "sqlite::memory:".to_string(),
},
nostr: NostrSettings {
nsec_privkey: "nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd"
.to_string(),
relays: vec!["wss://relay.test".to_string()],
},
mostro: MostroSettings::default(),
lightning: LightningSettings::default(),
rpc: RpcSettings::default(),
expiration: Some(ExpirationSettings::default()),
anti_abuse_bond: None,
}
}
}
#[cfg(test)]
mod tests {
use super::test_utils::{test_settings, MockOrderMsgQueue, TestContextBuilder};
use sqlx::SqlitePool;
use std::sync::Arc;
#[tokio::test]
async fn builder_can_inject_mock_order_message_queue() {
let pool = Arc::new(SqlitePool::connect("sqlite::memory:").await.unwrap());
let mock_queue = MockOrderMsgQueue::new();
let (ctx, mock_order_queue) = TestContextBuilder::new()
.with_pool(pool)
.with_settings(test_settings())
.with_mock_order_msg_queue(mock_queue)
.build_with_mocks();
let queue = mock_order_queue.expect("mock queue should be present");
assert!(Arc::ptr_eq(&queue.queue(), ctx.order_msg_queue()));
}
#[tokio::test]
async fn builder_can_use_custom_queue() {
let pool = Arc::new(SqlitePool::connect("sqlite::memory:").await.unwrap());
let queue: super::OrderMsgQueue = Arc::new(tokio::sync::RwLock::new(Vec::new()));
let ctx = TestContextBuilder::new()
.with_pool(pool)
.with_settings(test_settings())
.with_order_msg_queue(queue.clone())
.build();
assert!(Arc::ptr_eq(&queue, ctx.order_msg_queue()));
}
}