crates/monetize-site-buy/tests/checked_tickets_on_the_shop.rs
//! ★ **Both sites hold their grow doors to a checked, unspent ticket** (owner decision
//! 2026-10-26, item 1; A32-9: every site, there is no knob any more).
//!
//! A Gunnar-shaped and a Holger-shaped [`TestSite`] over the crate's fake ONE box: where the box
//! checks no tenant ticket ([`FakeOneBox::checks_no_tickets`]), the window, the grow, the status poll and the cancel
//! answer 501 with the buyer's words and no `Purchase` call is made; where it checks them
//! ([`FakeOneBox::verify_tickets`]), a fresh ticket grows the account and the same ticket again
//! is spent. The fake's own `Products.List` and spent rule are tested here too. Addresses are
//! `*.mock.invalid`.
mod sites;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use axum::http::StatusCode;
use monetize_proto as pb;
use monetize_site_buy::relay::RENEWAL_NOT_VERIFIED;
use monetize_site_buy::testkit::{FakeOneBox, BUNDLE, SPENT};
use monetize_site_buy::Relay;
use serde_json::json;
use sites::{Kit, Shape, TestSite, BEARER};
for_both_sites!(
a_grow_door_refuses_where_the_shop_checks_no_ticket,
a_checked_unspent_ticket_grows_the_account_and_is_then_spent
);
async fn kit(shape: Shape, verifies: bool) -> Kit {
let fake = FakeOneBox::spawn(shape.product(), BEARER).await;
if verifies {
fake.verify_tickets();
} else {
fake.checks_no_tickets();
}
fake.add_account("x14", "x14@mock.invalid", &[("disk_bytes", 5 << 30)]);
let link = fake.link();
let mut k = Kit::over(shape, fake, link, None);
// Each ticket with a fresh nonce, as the real sites mint them: the box spends each once.
let nonce = Arc::new(AtomicU64::new(0));
let mint = move |b: &monetize_site_buy::Buyer,
purpose: &str,
_: &std::collections::BTreeMap<String, u64>| {
format!(
"{}:{purpose}:{}",
b.account,
nonce.fetch_add(1, Ordering::SeqCst)
)
.into_bytes()
};
k.site = TestSite::over(shape, &k.fake).with_minter(Arc::new(mint));
k.relay = Arc::new(Relay::new(k.site.clone(), k.fake.link()));
k
}
async fn a_grow_door_refuses_where_the_shop_checks_no_ticket(shape: Shape) {
let k = kit(shape, false).await;
for (method, uri, body) in [
("GET", "/api/account/subscription/bundles", None),
(
"POST",
"/api/account/subscription/buy-bundle",
Some(json!({"bundle": BUNDLE})),
),
(
"GET",
"/api/account/subscription/status?reference=p-1",
None,
),
(
"POST",
"/api/account/subscription/cancel",
Some(json!({"reference": "p-1"})),
),
] {
let r = k.ask(method, uri, Some("x14"), body).await;
assert_eq!(
(r.status, r.error()),
(StatusCode::NOT_IMPLEMENTED, RENEWAL_NOT_VERIFIED),
"{shape:?} {method} {uri}: {:?}",
r.body
);
}
let b = k.fake.book();
assert!(
b.calls.iter().all(|c| !c.starts_with("Purchase.")),
"{shape:?}: no order reached the shop: {:?}",
b.calls
);
assert!(b.checkouts.is_empty());
}
async fn a_checked_unspent_ticket_grows_the_account_and_is_then_spent(shape: Shape) {
let k = kit(shape, true).await;
let window = k
.ask(
"GET",
"/api/account/subscription/bundles",
Some("x14"),
None,
)
.await;
assert_eq!(
window.status,
StatusCode::OK,
"{shape:?}: {:?}",
window.body
);
let buy = k
.ask(
"POST",
"/api/account/subscription/buy-bundle",
Some("x14"),
Some(json!({"bundle": BUNDLE})),
)
.await;
assert_eq!(
buy.status,
StatusCode::OK,
"{shape:?}: a fresh ticket is honoured: {:?}",
buy.body
);
assert_eq!(k.fake.book().checkouts_of("bundle").len(), 1);
let reference = buy.body["reference"].as_str().unwrap().to_owned();
let done = k
.ask(
"GET",
&format!("/api/account/subscription/status?reference={reference}"),
Some("x14"),
None,
)
.await;
assert_eq!(
done.status,
StatusCode::OK,
"{shape:?}: each poll carries a fresh ticket: {:?}",
done.body
);
// The order's ticket presented again is spent: the box buys nothing more for it.
let order = k
.fake
.book()
.tickets
.iter()
.find(|(c, _)| c == "Purchase.Start")
.unwrap()
.1
.clone();
let again = k
.fake
.link()
.purchase()
.start(pb::StartPurchaseRequest {
product: shape.product().into(),
tenant: "x14".into(),
bundle: BUNDLE.into(),
actor_ticket: order,
..Default::default()
})
.await;
let refused = again.expect_err("a spent ticket");
assert_eq!(
(refused.code(), refused.message()),
(tonic::Code::PermissionDenied, SPENT),
"{shape:?}"
);
assert_eq!(
k.fake.book().checkouts_of("bundle").len(),
1,
"{shape:?}: still one Stripe page"
);
}
/// The fake's `Products.List` says whether it checks tickets (as spawned it says so, as a
/// configured ONE box does), and once it verifies them its `Purchase` doors honour a ticket once.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_fake_one_box_says_whether_it_checks_tickets_and_spends_each_once() {
let fake = FakeOneBox::spawn("holger", BEARER).await;
let products = |fake: &FakeOneBox| {
let link = fake.link();
async move {
link.products()
.list(pb::ListProductsRequest {})
.await
.unwrap()
.into_inner()
.products
}
};
let p = products(&fake).await;
assert_eq!(
(p.len(), p[0].id.as_str(), p[0].tenant_renewal),
(1, "holger", true),
"as spawned it says it checks them"
);
let start = |ticket: &'static [u8]| {
let link = fake.link();
async move {
link.purchase()
.start(pb::StartPurchaseRequest {
product: "holger".into(),
tenant: "x14".into(),
bundle: BUNDLE.into(),
actor_ticket: ticket.to_vec(),
..Default::default()
})
.await
}
};
assert!(
start(b"t1").await.is_ok() && start(b"t1").await.is_ok(),
"as spawned it spends none"
);
fake.checks_no_tickets();
assert!(
!products(&fake).await[0].tenant_renewal,
"a shop older than the ticket"
);
fake.verify_tickets();
assert!(start(b"t2").await.is_ok(), "a fresh ticket is honoured");
let spent = start(b"t2").await.expect_err("the same ticket again");
assert_eq!(
(spent.code(), spent.message()),
(tonic::Code::PermissionDenied, SPENT)
);
}