use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use actix_web::{http::header, test};
use async_trait::async_trait;
use bytes::Bytes;
use serde_json::Value;
use solid_pod_rs::auth::nip98;
use solid_pod_rs::error::PodError;
use solid_pod_rs::storage::memory::MemoryBackend;
use solid_pod_rs::storage::{ResourceMeta, Storage, StorageEvent};
use solid_pod_rs_server::{build_app, AppState};
struct YieldingStorage(MemoryBackend);
#[async_trait]
impl Storage for YieldingStorage {
async fn get(&self, path: &str) -> Result<(Bytes, ResourceMeta), PodError> {
tokio::task::yield_now().await;
self.0.get(path).await
}
async fn put(&self, path: &str, body: Bytes, ct: &str) -> Result<ResourceMeta, PodError> {
self.0.put(path, body, ct).await
}
async fn delete(&self, path: &str) -> Result<(), PodError> {
self.0.delete(path).await
}
async fn list(&self, container: &str) -> Result<Vec<String>, PodError> {
self.0.list(container).await
}
async fn head(&self, path: &str) -> Result<ResourceMeta, PodError> {
self.0.head(path).await
}
async fn exists(&self, path: &str) -> Result<bool, PodError> {
self.0.exists(path).await
}
async fn watch(
&self,
path: &str,
) -> Result<tokio::sync::mpsc::Receiver<StorageEvent>, PodError> {
self.0.watch(path).await
}
}
const SECRET: &str = "5555555555555555555555555555555555555555555555555555555555555555";
fn auth(body: &[u8], offset: u64) -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
.saturating_sub(10)
+ offset;
let token = nip98::mint_with_payload(
"http://localhost:8080/pay/.sell",
"POST",
Some(body),
SECRET,
now,
)
.unwrap();
format!("Nostr {token}")
}
#[actix_web::test]
async fn concurrent_payment_state_writes_lose_no_update() {
let storage = Arc::new(YieldingStorage(MemoryBackend::new()));
let app = test::init_service(build_app(AppState::new(storage))).await;
let sell = |price: u64, offset: u64| {
let body = format!(
r#"{{"sell_currency":"tbtc3","sell_amount":10,"buy_currency":"sat","price":{price}}}"#
);
test::TestRequest::post()
.uri("/pay/.sell")
.insert_header((header::AUTHORIZATION, auth(body.as_bytes(), offset)))
.insert_header((header::CONTENT_TYPE, "application/json"))
.set_payload(body)
.to_request()
};
let (first, second) = futures_util::future::join(
test::call_service(&app, sell(100, 0)),
test::call_service(&app, sell(200, 1)),
)
.await;
assert_eq!(first.status().as_u16(), 200);
assert_eq!(second.status().as_u16(), 200);
let offers = test::call_service(
&app,
test::TestRequest::get().uri("/pay/.offers").to_request(),
)
.await;
let offers: Value = test::read_body_json(offers).await;
let mut prices: Vec<u64> = offers
.as_array()
.unwrap()
.iter()
.map(|o| o["price"].as_u64().unwrap())
.collect();
prices.sort_unstable();
assert_eq!(prices, [100, 200], "both concurrent orders must persist");
}