use axum_test::TestServer;
use fts_core::{
models::{AuctionOutcome, AuthRecord, BidderId, DateTimeRangeResponse, ProductId},
ports::MarketRepository,
};
use fts_server::{CustomJWTClaims, router, state};
use jwt_simple::prelude::{Claims, Duration, HS256Key, MACLike};
use rstest::rstest;
use rstest_reuse::apply;
use serde_json::json;
use std::any::Any;
use std::future::Future;
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use uuid::Uuid;
mod backends;
use backends::*;
#[apply(all_backends)]
#[rstest]
async fn roundtrip(#[case] backend: impl Future<Output = (impl MarketRepository, Box<dyn Any>)>) {
let (db, _container) = backend.await;
let app = {
let state = state("secret", db);
router(state.0)
};
let admin_token = {
let key = HS256Key::from_bytes("secret".as_bytes());
let account: BidderId = Uuid::new_v4().into();
let account_str = account.to_string();
let claims = Claims::with_custom_claims(
CustomJWTClaims { admin: true },
Duration::from_days(365 * 20 + 5),
)
.with_subject(&account_str);
key.authenticate(claims).unwrap()
};
let account_tokens = (0..3)
.map(|_| {
let key = HS256Key::from_bytes("secret".as_bytes());
let account: BidderId = Uuid::new_v4().into();
let account_str = account.to_string();
let claims =
Claims::create(Duration::from_days(365 * 20 + 5)).with_subject(&account_str);
(account, key.authenticate(claims).unwrap())
})
.collect::<Vec<_>>();
let server = TestServer::new(app).unwrap();
let (apple, banana, carrot, daikon) = {
let (from, thru) = {
let from = OffsetDateTime::now_utc();
let thru = from + std::time::Duration::from_secs(60);
(
from.format(&Rfc3339).unwrap(),
thru.format(&Rfc3339).unwrap(),
)
};
let produce = server
.post("/admin/products")
.authorization_bearer(&admin_token)
.json(&json!([
{ "kind": "APPLE", "from": from, "thru": thru },
{ "kind": "BANANA", "from": from, "thru": thru },
{ "kind": "CARROT", "from": from, "thru": thru },
{ "kind": "DAIKON", "from": from, "thru": thru },
]))
.await
.json::<Vec<ProductId>>();
(produce[0], produce[1], produce[2], produce[3])
};
let fruit_id = {
let response = server
.post("/v0/auths")
.authorization_bearer(&account_tokens[0].1)
.json(&json!({
"portfolio": {
apple: 1.0,
banana: 1.0
},
"data": {
"demand": [
{ "rate": -10.0, "price": 10.0 },
{ "rate": 0.0, "price": 5.0 }
],
"min_trade": serde_json::Value::Null,
"max_trade": serde_json::Value::Null,
}
}))
.await
.json::<AuthRecord>();
response.auth_id
};
let veggie_id = {
let response = server
.post("/v0/auths")
.authorization_bearer(&account_tokens[1].1)
.json(&json!({
"portfolio": {
carrot: 1.0,
daikon: 1.0
},
"data": {
"demand": [
{ "rate": -10.0, "price": 10.0 },
{ "rate": 0.0, "price": 5.0 }
],
"min_trade": serde_json::Value::Null,
"max_trade": serde_json::Value::Null,
}
}))
.await
.json::<AuthRecord>();
response.auth_id
};
let produce_id = {
let response = server
.post("/v0/auths")
.authorization_bearer(&account_tokens[2].1)
.json(&json!({
"portfolio": {
apple: 1.0,
banana: 1.0,
carrot: 1.0,
daikon: 1.0,
},
"data":{
"demand": [
{ "rate": 0.0, "price": 30.0 },
{ "rate": 10.0, "price": 0.0 }
],
"min_trade": serde_json::Value::Null,
"max_trade": serde_json::Value::Null,
}
}))
.await
.json::<AuthRecord>();
response.auth_id
};
let (from, thru) = {
let from = OffsetDateTime::now_utc() + std::time::Duration::from_secs(60);
let thru = from + std::time::Duration::from_secs(60 * 60);
(from, thru)
};
server
.post("/admin/auctions/solve")
.authorization_bearer(&admin_token)
.json(&json!({
"from": from.format(&Rfc3339).unwrap(),
"thru": thru.format(&Rfc3339).unwrap(),
}))
.await;
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
{
let response = server
.get(&format!("/v0/auths/{}/outcomes", fruit_id))
.authorization_bearer(&account_tokens[0].1)
.await
.json::<DateTimeRangeResponse<AuctionOutcome>>();
assert_eq!(response.results.len(), 1);
assert_eq!((response.results[0].outcome.trade * 100.0).round(), -500.0);
}
{
let response = server
.get(&format!("/v0/auths/{}/outcomes", veggie_id))
.authorization_bearer(&account_tokens[1].1)
.await
.json::<DateTimeRangeResponse<AuctionOutcome>>();
assert_eq!(response.results.len(), 1);
assert_eq!((response.results[0].outcome.trade * 100.0).round(), -500.0);
}
{
let response = server
.get(&format!("/v0/auths/{}/outcomes", produce_id))
.authorization_bearer(&account_tokens[2].1)
.await
.json::<DateTimeRangeResponse<AuctionOutcome>>();
assert_eq!(response.results.len(), 1);
assert_eq!((response.results[0].outcome.trade * 100.0).round(), 500.0);
}
for product in [apple, banana, carrot, daikon] {
let response = server
.get(&format!("/v0/products/{}/outcomes", product))
.await
.json::<DateTimeRangeResponse<AuctionOutcome>>();
assert_eq!(response.results.len(), 1);
let price = (response.results[0].outcome.price * 100.0).round();
assert_eq!(price, 375.0);
}
}