crates/monetize-site-buy/tests/common/mod.rs
//! A fake ONE monetize (Signup, EstateOrder, Purchase over monetize-proto, on a loopback
//! port) and a fake product site, for the relay's tests. Addresses are `*.mock.invalid`.
#![allow(dead_code)]
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use axum::http::{HeaderMap, StatusCode};
use monetize_proto as pb;
use monetize_site_buy::{Account, BuySite, Buyer, Door, Member, Refusal, Relay, ShopLink, Who};
use tonic::{Request, Response, Status};
pub const BEARER: &str = "site-bearer-for-tests";
pub const ORIGIN: &str = "https://holger.mock.invalid";
/// What the fake heard: every request, and the bearer it came with.
#[derive(Default)]
pub struct Heard {
pub bearers: Vec<String>,
pub signup_start: Option<pb::StartSignupRequest>,
pub estate_start: Option<pb::EstateOrderStartRequest>,
pub change: Option<pb::EstateOrderChangePlanRequest>,
pub status_ticket: Option<Vec<u8>>,
pub purchase_start: Option<pb::StartPurchaseRequest>,
pub bundles: Option<pb::ListBundlesRequest>,
pub sign_in_again: Option<pb::SignupSignInAgainRequest>,
pub purchases: Option<pb::EstateOrderPurchasesRequest>,
}
#[derive(Clone, Default)]
pub struct Fake {
pub heard: Arc<Mutex<Heard>>,
/// The username `Signup.Status` answers CREATED for.
pub created_user: Arc<Mutex<String>>,
pub product: String,
}
impl Fake {
fn hear<T>(&self, r: &Request<T>) {
let b = r
.metadata()
.get("authorization")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_owned();
self.heard.lock().unwrap().bearers.push(b);
}
}
#[tonic::async_trait]
impl pb::signup_server::Signup for Fake {
async fn status(
&self,
r: Request<pb::SignupStatusRequest>,
) -> Result<Response<pb::SignupStatusResponse>, Status> {
self.hear(&r);
let user = self.created_user.lock().unwrap().clone();
if user.is_empty() {
return Ok(Response::new(pb::SignupStatusResponse {
state: pb::signup_status_response::State::Pending as i32,
words: "Waiting for your payment to be confirmed. Nothing more is needed from you."
.into(),
reason: "operator words".into(),
..Default::default()
}));
}
Ok(Response::new(pb::SignupStatusResponse {
state: pb::signup_status_response::State::Created as i32,
username: user,
product: self.product.clone(),
..Default::default()
}))
}
async fn check_name(
&self,
r: Request<pb::CheckNameRequest>,
) -> Result<Response<pb::CheckNameResponse>, Status> {
self.hear(&r);
let r = r.into_inner();
let free = r.username != "taken";
Ok(Response::new(pb::CheckNameResponse {
free,
reason: if free {
String::new()
} else {
format!("username {:?} is taken; choose another", r.username)
},
}))
}
async fn start(
&self,
r: Request<pb::StartSignupRequest>,
) -> Result<Response<pb::StartSignupResponse>, Status> {
self.hear(&r);
let r = r.into_inner();
if r.username == "nosink" {
return Err(Status::unimplemented("no checkout here"));
}
if r.username == "held" {
return Err(Status::failed_precondition("Someone is signing up with this account name right now. Choose another name, or try again in half an hour."));
}
self.heard.lock().unwrap().signup_start = Some(r);
Ok(Response::new(pb::StartSignupResponse {
url: "https://checkout.mock.invalid/c/pay/cs_test_abcdefghijkl".into(),
session: "cs_test_abcdefghijkl".into(),
}))
}
async fn rename(
&self,
r: Request<pb::RenameSignupRequest>,
) -> Result<Response<pb::RenameSignupResponse>, Status> {
self.hear(&r);
Err(Status::not_found("no such signup"))
}
async fn plans(
&self,
r: Request<pb::SignupPlansRequest>,
) -> Result<Response<pb::SignupPlansResponse>, Status> {
self.hear(&r);
let market = r.into_inner().market;
Ok(Response::new(pb::SignupPlansResponse {
plans: vec![
pb::SignupPlanOffer {
id: format!("{}/account", self.product),
name: "Free account".into(),
price_words: "Free".into(),
..Default::default()
},
pb::SignupPlanOffer {
id: format!("{}/5gb-1m", self.product),
name: "5 GB".into(),
price_words: "100.00 SEK a month".into(),
period_days: 30,
..Default::default()
},
],
market,
}))
}
async fn send_sign_in_again(
&self,
r: Request<pb::SignupSignInAgainRequest>,
) -> Result<Response<pb::SignupSignInAgainResponse>, Status> {
self.hear(&r);
self.heard.lock().unwrap().sign_in_again = Some(r.into_inner());
Ok(Response::new(pb::SignupSignInAgainResponse {
words: "We’ve sent it again to o•••@m•••.invalid.".into(),
}))
}
}
#[tonic::async_trait]
impl pb::estate_order_server::EstateOrder for Fake {
async fn product_order(
&self,
_: tonic::Request<pb::EstateProductOrderRequest>,
) -> Result<tonic::Response<pb::EstateProductOrderResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("product_order"))
}
async fn product_order_answer(
&self,
_: tonic::Request<pb::EstateProductOrderAnswerRequest>,
) -> Result<tonic::Response<pb::EstateProductOrderAnswerResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("product_order_answer"))
}
async fn product_order_kinds(
&self,
_: tonic::Request<pb::EstateProductOrderKindsRequest>,
) -> Result<tonic::Response<pb::EstateProductOrderKindsResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("product_order_kinds"))
}
async fn start(
&self,
r: Request<pb::EstateOrderStartRequest>,
) -> Result<Response<pb::EstateOrderStartResponse>, Status> {
self.hear(&r);
let r = r.into_inner();
if r.company == "taken" {
return Err(Status::already_exists("taken.holger.mock.invalid is taken"));
}
self.heard.lock().unwrap().estate_start = Some(r.clone());
Ok(Response::new(pb::EstateOrderStartResponse {
reference: format!("estate/{}", r.company),
checkout_url: "https://checkout.mock.invalid/c/pay/cs_test_estate".into(),
state: "waiting_for_payment".into(),
words: "Waiting for your payment on the payment page.".into(),
price_words: "385.20 EUR".into(),
}))
}
async fn status(
&self,
r: Request<pb::EstateOrderStatusRequest>,
) -> Result<Response<pb::EstateOrderStatusResponse>, Status> {
self.hear(&r);
let r = r.into_inner();
self.heard.lock().unwrap().status_ticket = Some(r.actor_ticket.clone());
Ok(Response::new(pb::EstateOrderStatusResponse {
company: r.company,
state: "queued".into(),
words: "Your Holger is paid and being prepared.".into(),
..Default::default()
}))
}
async fn send_sign_in_again(
&self,
r: Request<pb::EstateOrderSignInAgainRequest>,
) -> Result<Response<pb::EstateOrderSignInAgainResponse>, Status> {
self.hear(&r);
Ok(Response::new(pb::EstateOrderSignInAgainResponse {
words: "sent".into(),
}))
}
async fn change_plan(
&self,
r: Request<pb::EstateOrderChangePlanRequest>,
) -> Result<Response<pb::EstateOrderChangePlanResponse>, Status> {
self.hear(&r);
let r = r.into_inner();
self.heard.lock().unwrap().change = Some(r.clone());
Ok(Response::new(pb::EstateOrderChangePlanResponse {
reference: format!("estate/{}/change-1", r.company),
checkout_url: "https://checkout.mock.invalid/c/pay/cs_test_change".into(),
status: "ordered".into(),
words: "held now".into(),
price_words: "12.00 EUR".into(),
twin_plan: String::new(),
}))
}
async fn ladder(
&self,
r: Request<pb::EstateOrderLadderRequest>,
) -> Result<Response<pb::EstateOrderLadderResponse>, Status> {
self.hear(&r);
Ok(Response::new(pb::EstateOrderLadderResponse {
steps: vec![pb::EstateLadderStep {
name: "Mini".into(),
sizes: Some(pb::EstateSizes {
front_plan: "f".into(),
main_plan: "m".into(),
main_data_gib: 40,
..Default::default()
}),
monthly_words: "96.20 EUR a month".into(),
available: true,
..Default::default()
}],
apex: "holger.mock.invalid".into(),
..Default::default()
}))
}
async fn sells(
&self,
r: Request<pb::EstateOrderSellsRequest>,
) -> Result<Response<pb::EstateOrderSellsResponse>, Status> {
self.hear(&r);
Ok(Response::new(pb::EstateOrderSellsResponse {
estates: true,
}))
}
async fn renew(
&self,
_: Request<pb::EstateOrderRenewRequest>,
) -> Result<Response<pb::EstateOrderRenewResponse>, Status> {
Err(Status::unimplemented("Renew"))
}
async fn purchases(
&self,
r: Request<pb::EstateOrderPurchasesRequest>,
) -> Result<Response<pb::EstateOrderPurchasesResponse>, Status> {
self.hear(&r);
let r = r.into_inner();
self.heard.lock().unwrap().purchases = Some(r.clone());
if r.account != "x14" {
return Ok(Response::new(pb::EstateOrderPurchasesResponse::default()));
}
Ok(Response::new(pb::EstateOrderPurchasesResponse {
estates: vec![pb::EstatePurchase {
company: "acme".into(),
host: "acme.holger.mock.invalid".into(),
url: "https://acme.holger.mock.invalid".into(),
state: "ready".into(),
step: "Mini".into(),
..Default::default()
}],
rooms: vec![],
account_here: true,
}))
}
async fn notice(
&self,
_: Request<pb::EstateNoticeRequest>,
) -> Result<Response<pb::EstateNoticeResponse>, Status> {
Err(Status::unimplemented("Notice"))
}
async fn cancel(
&self,
_: Request<pb::EstateOrderCancelRequest>,
) -> Result<Response<pb::EstateOrderCancelResponse>, Status> {
Err(Status::unimplemented("Cancel"))
}
async fn settle(
&self,
_: Request<pb::EstateOrderSettleRequest>,
) -> Result<Response<pb::EstateOrderSettleResponse>, Status> {
Err(Status::unimplemented("Settle"))
}
async fn restore_front(
&self,
_: Request<pb::EstateOrderRestoreFrontRequest>,
) -> Result<Response<pb::EstateOrderRestoreFrontResponse>, Status> {
Err(Status::unimplemented("RestoreFront"))
}
async fn terminate(
&self,
_: Request<pb::EstateOrderTerminateRequest>,
) -> Result<Response<pb::EstateOrderTerminateResponse>, Status> {
Err(Status::unimplemented("Terminate"))
}
async fn first_link(
&self,
_: Request<pb::EstateOrderFirstLinkRequest>,
) -> Result<Response<pb::EstateOrderFirstLinkResponse>, Status> {
Err(Status::unimplemented("FirstLink"))
}
async fn refeed(
&self,
_: Request<pb::EstateOrderRefeedRequest>,
) -> Result<Response<pb::EstateOrderRefeedResponse>, Status> {
Err(Status::unimplemented("Refeed"))
}
async fn rebuild_main(
&self,
_: Request<pb::EstateRebuildMainRequest>,
) -> Result<Response<pb::EstateRebuildMainResponse>, Status> {
Err(Status::unimplemented("RebuildMain"))
}
async fn order_site(
&self,
_: Request<pb::EstateOrderSiteRequest>,
) -> Result<Response<pb::EstateOrderSiteResponse>, Status> {
Err(Status::unimplemented("OrderSite"))
}
async fn list_sites(
&self,
_: Request<pb::EstateSitesRequest>,
) -> Result<Response<pb::EstateSitesResponse>, Status> {
Err(Status::unimplemented("ListSites"))
}
async fn update_estate(
&self,
_: Request<pb::EstateUpdateRequest>,
) -> Result<Response<pb::EstateUpdateResponse>, Status> {
Err(Status::unimplemented("UpdateEstate"))
}
async fn update_status(
&self,
_: Request<pb::EstateUpdateStatusRequest>,
) -> Result<Response<pb::EstateUpdateResponse>, Status> {
Err(Status::unimplemented("UpdateStatus"))
}
async fn cancel_update(
&self,
_: Request<pb::EstateUpdateStatusRequest>,
) -> Result<Response<pb::EstateUpdateResponse>, Status> {
Err(Status::unimplemented("cancel_update"))
}
async fn set_live_update(
&self,
_: Request<pb::EstateSetLiveUpdateRequest>,
) -> Result<Response<pb::EstateUpdateResponse>, Status> {
Err(Status::unimplemented("SetLiveUpdate"))
}
async fn adopt_estate(
&self,
_: Request<pb::EstateAdoptRequest>,
) -> Result<Response<pb::EstateAdoptResponse>, Status> {
Err(Status::unimplemented("AdoptEstate"))
}
async fn list_code_red(
&self,
_: Request<pb::CodeRedListRequest>,
) -> Result<Response<pb::CodeRedList>, Status> {
Err(Status::unimplemented("ListCodeRed"))
}
async fn ack_code_red(
&self,
_: Request<pb::CodeRedAckRequest>,
) -> Result<Response<pb::CodeRedList>, Status> {
Err(Status::unimplemented("AckCodeRed"))
}
async fn raise_code_red(
&self,
_: Request<pb::CodeRed>,
) -> Result<Response<pb::CodeRedList>, Status> {
Err(Status::unimplemented("RaiseCodeRed"))
}
async fn clear_code_red(
&self,
_: Request<pb::CodeRedClearRequest>,
) -> Result<Response<pb::CodeRedList>, Status> {
Err(Status::unimplemented("ClearCodeRed"))
}
async fn narrate_log(
&self,
_: Request<pb::EstateNarrateLogRequest>,
) -> Result<Response<pb::EstateNarrateLogResponse>, Status> {
Err(Status::unimplemented("NarrateLog"))
}
async fn install_log(
&self,
_: Request<pb::EstateInstallLogRequest>,
) -> Result<Response<pb::EstateInstallLogResponse>, Status> {
Err(Status::unimplemented("InstallLog"))
}
}
#[tonic::async_trait]
impl pb::products_server::Products for Fake {
/// A32-9: every site's grow doors ask first whether the shop checks tenant tickets: it does.
async fn list(
&self,
_: Request<pb::ListProductsRequest>,
) -> Result<Response<pb::ListProductsResponse>, Status> {
Ok(Response::new(pb::ListProductsResponse {
products: vec![pb::ProductInfo {
id: self.product.clone(),
tenant_renewal: true,
..Default::default()
}],
..Default::default()
}))
}
}
#[tonic::async_trait]
impl pb::purchase_server::Purchase for Fake {
async fn bundles(
&self,
r: Request<pb::ListBundlesRequest>,
) -> Result<Response<pb::ListBundlesResponse>, Status> {
self.hear(&r);
self.heard.lock().unwrap().bundles = Some(r.into_inner());
Ok(Response::new(pb::ListBundlesResponse {
bundles: vec![
pb::Bundle {
id: "more10".into(),
label: "10.0 GiB more room".into(),
price: "220.00 SEK".into(),
period: "month".into(),
delta: [("disk_bytes".to_string(), 10u64)].into_iter().collect(),
can_serve: true,
..Default::default()
},
pb::Bundle {
id: "huge".into(),
label: "1 TiB more room".into(),
can_serve: false,
reason: "no room".into(),
..Default::default()
},
],
reading: false,
plan: Some(pb::PlanStatus {
state: "none".into(),
..Default::default()
}),
}))
}
async fn start(
&self,
r: Request<pb::StartPurchaseRequest>,
) -> Result<Response<pb::StartPurchaseResponse>, Status> {
self.hear(&r);
self.heard.lock().unwrap().purchase_start = Some(r.into_inner());
Ok(Response::new(pb::StartPurchaseResponse {
reference: "p-1".into(),
state: pb::PurchaseState::Pending as i32,
redirect_url: "https://checkout.mock.invalid/c/pay/cs_test_room".into(),
..Default::default()
}))
}
async fn status(
&self,
r: Request<pb::PurchaseStatusRequest>,
) -> Result<Response<pb::PurchaseStatusResponse>, Status> {
self.hear(&r);
let r = r.into_inner();
Ok(Response::new(pb::PurchaseStatusResponse {
reference: r.reference,
state: pb::PurchaseState::Paid as i32,
phase: pb::PurchasePhase::Done as i32,
..Default::default()
}))
}
async fn quote(
&self,
_: Request<pb::QuoteRequest>,
) -> Result<Response<pb::QuoteResponse>, Status> {
Err(Status::unimplemented("Quote"))
}
async fn abandon(
&self,
_: Request<pb::AbandonPurchaseRequest>,
) -> Result<Response<pb::AbandonPurchaseResponse>, Status> {
Err(Status::unimplemented("Abandon"))
}
async fn cancel(
&self,
_: Request<pb::CancelPurchaseRequest>,
) -> Result<Response<pb::CancelPurchaseResponse>, Status> {
Err(Status::unimplemented("Cancel"))
}
async fn history(
&self,
_: Request<pb::PurchaseHistoryRequest>,
) -> Result<Response<pb::PurchaseHistoryResponse>, Status> {
Err(Status::unimplemented("History"))
}
}
/// **The fake product site**: `x-test-user` names who asks (absent: a visitor; `machine`;
/// `member:<account>`; else the account).
#[derive(Clone)]
pub struct Site {
pub word: String,
/// The site's fixed buy link, where the shop mints no checkout.
pub fixed: Option<String>,
}
impl BuySite for Site {
fn origin(&self) -> String {
ORIGIN.to_owned()
}
fn product_word(&self) -> String {
self.word.clone()
}
fn fixed_buy_url(&self) -> Option<String> {
self.fixed.clone()
}
async fn who(&self, headers: &HeaderMap) -> Result<Who, Refusal> {
Ok(
match headers.get("x-test-user").and_then(|v| v.to_str().ok()) {
None => Who::Visitor,
Some("machine") => Who::Machine,
Some("down") => {
return Err(Refusal::new(
StatusCode::SERVICE_UNAVAILABLE,
"Holger cannot be reached right now: try again in a minute.",
))
}
Some(m) if m.starts_with("member:") => Who::Person(Buyer {
account: "ann".into(),
member: Some(Member {
account: m[7..].into(),
admin: "boss".into(),
line: format!("You are a member of {}.", &m[7..]),
}),
..Default::default()
}),
Some(a) => Who::Person(Buyer {
account: a.into(),
..Default::default()
}),
},
)
}
async fn ticket(
&self,
_: &HeaderMap,
buyer: &Buyer,
purpose: &str,
caps: &BTreeMap<String, u64>,
) -> Result<Vec<u8>, Refusal> {
Ok(format!("{purpose}:{}:{caps:?}", buyer.account).into_bytes())
}
async fn held_caps(&self, _: &HeaderMap, _: &Buyer) -> Result<BTreeMap<String, u64>, Refusal> {
Ok(BTreeMap::from([("disk_bytes".to_string(), 5u64)]))
}
async fn signup_account(&self, user: &str) -> Result<Account, Refusal> {
match user {
"done" => Ok(Account::HasPasskey),
"x14" => Ok(Account::NoPasskey),
_ => Err(Refusal::new(StatusCode::CONFLICT, "not here yet")),
}
}
}
/// A fake monetize on a loopback port and a relay to it.
pub async fn relay(product: &str) -> (Arc<Relay<Site>>, Fake) {
relay_with(product, None).await
}
/// [`relay`] for a site with a fixed buy link.
pub async fn relay_with(product: &str, fixed: Option<&str>) -> (Arc<Relay<Site>>, Fake) {
let fake = Fake {
product: product.into(),
..Fake::default()
};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let at = listener.local_addr().unwrap();
let serve = tonic::transport::Server::builder()
.add_service(pb::signup_server::SignupServer::new(fake.clone()))
.add_service(pb::estate_order_server::EstateOrderServer::new(
fake.clone(),
))
.add_service(pb::purchase_server::PurchaseServer::new(fake.clone()))
.add_service(pb::products_server::ProductsServer::new(fake.clone()))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener));
tokio::spawn(serve);
let shop = ShopLink::connect_lazy(
&Door {
url: format!("http://{at}"),
..Door::default()
},
BEARER,
product,
)
.unwrap();
(
Arc::new(Relay::new(
Site {
word: "Holger".into(),
fixed: fixed.map(str::to_owned),
},
shop,
)),
fake,
)
}
/// One request through the relay's router: `(status, json body, location)`.
pub async fn call(
r: &Arc<Relay<Site>>,
method: &str,
uri: &str,
user: Option<&str>,
body: Option<serde_json::Value>,
) -> (StatusCode, serde_json::Value, Option<String>) {
use tower::ServiceExt;
let mut req = axum::http::Request::builder()
.method(method)
.uri(uri)
.header("accept-language", "sv-SE");
if let Some(u) = user {
req = req.header("x-test-user", u);
}
let req = match body {
Some(b) => req
.header("content-type", "application/json")
.body(axum::body::Body::from(b.to_string()))
.unwrap(),
None => req.body(axum::body::Body::empty()).unwrap(),
};
let res = monetize_site_buy::router(r.clone())
.oneshot(req)
.await
.unwrap();
let status = res.status();
let location = res
.headers()
.get("location")
.and_then(|v| v.to_str().ok())
.map(str::to_owned);
let bytes = axum::body::to_bytes(res.into_body(), 1 << 20)
.await
.unwrap();
(
status,
serde_json::from_slice(&bytes).unwrap_or(serde_json::Value::Null),
location,
)
}