crates/monetize-site-buy/tests/a16_renewal_offered.rs
//! ★ **A renewal is offered only where monetize verifies the tenant's renewal ticket** (A16,
//! Gunnar's console's `api::subscription::offered`, moved as it is): before Prolong's quote, the
//! renewal and the history, the shop's `Products.List` must say this product checks tenant
//! renewals; otherwise the door answers 501 with Gunnar's sentence and the purchase is never
//! asked. Addresses are `*.mock.invalid`.
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, Refusal, Relay, ShopLink, Who};
use tonic::{Request, Response, Status};
#[derive(Clone, Default)]
struct Shop {
renewal: bool,
asked: Arc<Mutex<u32>>,
}
#[tonic::async_trait]
impl pb::products_server::Products for Shop {
async fn list(
&self,
_: Request<pb::ListProductsRequest>,
) -> Result<Response<pb::ListProductsResponse>, Status> {
Ok(Response::new(pb::ListProductsResponse {
products: vec![pb::ProductInfo {
id: "holger".into(),
tenant_renewal: self.renewal,
..Default::default()
}],
..Default::default()
}))
}
}
#[tonic::async_trait]
impl pb::purchase_server::Purchase for Shop {
async fn bundles(
&self,
_: Request<pb::ListBundlesRequest>,
) -> Result<Response<pb::ListBundlesResponse>, Status> {
Err(Status::unimplemented("Bundles"))
}
async fn start(
&self,
_: Request<pb::StartPurchaseRequest>,
) -> Result<Response<pb::StartPurchaseResponse>, Status> {
*self.asked.lock().unwrap() += 1;
Err(Status::failed_precondition("nothing to renew"))
}
async fn status(
&self,
_: Request<pb::PurchaseStatusRequest>,
) -> Result<Response<pb::PurchaseStatusResponse>, Status> {
Err(Status::unimplemented("Status"))
}
async fn quote(
&self,
_: Request<pb::QuoteRequest>,
) -> Result<Response<pb::QuoteResponse>, Status> {
*self.asked.lock().unwrap() += 1;
Ok(Response::new(pb::QuoteResponse::default()))
}
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> {
*self.asked.lock().unwrap() += 1;
Ok(Response::new(pb::PurchaseHistoryResponse::default()))
}
}
struct Site;
impl BuySite for Site {
fn origin(&self) -> String {
"https://holger.mock.invalid".into()
}
fn product_word(&self) -> String {
"Holger".into()
}
async fn who(&self, _: &HeaderMap) -> Result<Who, Refusal> {
Ok(Who::Person(Buyer {
account: "x14".into(),
..Default::default()
}))
}
async fn ticket(
&self,
_: &HeaderMap,
_: &Buyer,
purpose: &str,
_: &BTreeMap<String, u64>,
) -> Result<Vec<u8>, Refusal> {
Ok(purpose.as_bytes().to_vec())
}
async fn held_caps(&self, _: &HeaderMap, _: &Buyer) -> Result<BTreeMap<String, u64>, Refusal> {
Ok(BTreeMap::new())
}
async fn signup_account(&self, _: &str) -> Result<Account, Refusal> {
Ok(Account::NoPasskey)
}
}
async fn relay(renewal: bool) -> (Arc<Relay<Site>>, Shop) {
let shop = Shop {
renewal,
..Shop::default()
};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let at = listener.local_addr().unwrap();
tokio::spawn(
tonic::transport::Server::builder()
.add_service(pb::products_server::ProductsServer::new(shop.clone()))
.add_service(pb::purchase_server::PurchaseServer::new(shop.clone()))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener)),
);
let link = ShopLink::connect_lazy(
&Door {
url: format!("http://{at}"),
..Door::default()
},
"site-bearer-for-tests",
"holger",
)
.unwrap();
(Arc::new(Relay::new(Site, link)), shop)
}
async fn call(r: &Arc<Relay<Site>>, method: &str, uri: &str) -> (StatusCode, serde_json::Value) {
use tower::ServiceExt;
let req = axum::http::Request::builder()
.method(method)
.uri(uri)
.header("content-type", "application/json")
.body(axum::body::Body::from(if method == "POST" {
"{}"
} else {
""
}))
.unwrap();
let res = monetize_site_buy::router(r.clone())
.oneshot(req)
.await
.unwrap();
let status = res.status();
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),
)
}
#[tokio::test]
async fn no_renewal_where_the_shop_does_not_verify_renewal_tickets() {
let (r, shop) = relay(false).await;
for (m, uri) in [
("GET", "/api/account/subscription"),
("POST", "/api/account/subscription/renew"),
("GET", "/api/account/subscription/history"),
] {
let (st, body) = call(&r, m, uri).await;
assert_eq!(st, StatusCode::NOT_IMPLEMENTED, "{uri}: {body}");
assert_eq!(
body["error"],
monetize_site_buy::relay::RENEWAL_NOT_VERIFIED,
"{uri}"
);
}
assert_eq!(
*shop.asked.lock().unwrap(),
0,
"the purchase is never asked"
);
}
#[tokio::test]
async fn a_renewal_goes_on_where_the_shop_verifies_them() {
let (r, shop) = relay(true).await;
assert_eq!(
call(&r, "GET", "/api/account/subscription").await.0,
StatusCode::OK
);
assert_eq!(
call(&r, "GET", "/api/account/subscription/history").await.0,
StatusCode::OK
);
assert_eq!(*shop.asked.lock().unwrap(), 2);
}