crates/monetize-site-buy/tests/h15_stepped_up_refuses.rs
//! **BuySite's `stepped_up` default REFUSES** (A28 H15). A site that does not prove a fresh key
//! touch can end no estate: the default answers `Err(STEP_UP_WORDS)`, never `Ok(())`. A24 once
//! sketched `Ok(())`; copying it would let a Terminate pass without a fresh touch. The trait's
//! method count is held here too, so the plan and the docs cannot drift from the code again.
use std::collections::BTreeMap;
use axum::http::HeaderMap;
use monetize_site_buy::site::STEP_UP_WORDS;
use monetize_site_buy::{Account, BuySite, Buyer, Refusal, Who};
/// A site that writes only the required methods: every default is the trait's own.
struct Bare;
impl BuySite for Bare {
fn origin(&self) -> String {
"https://bare.mock.invalid".into()
}
fn product_word(&self) -> String {
"Bare".into()
}
async fn who(&self, _: &HeaderMap) -> Result<Who, Refusal> {
Ok(Who::Visitor)
}
async fn ticket(
&self,
_: &HeaderMap,
_: &Buyer,
_: &str,
_: &BTreeMap<String, u64>,
) -> Result<Vec<u8>, Refusal> {
Ok(Vec::new())
}
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)
}
}
#[tokio::test]
async fn the_default_stepped_up_refuses_with_the_step_up_words() {
let mut fresh = HeaderMap::new();
fresh.insert("x-test-user", "fresh:acme".parse().unwrap());
for headers in [HeaderMap::new(), fresh] {
assert_eq!(
Bare.stepped_up(&headers).await,
Err(STEP_UP_WORDS.to_owned()),
"the default never says yes"
);
}
}
/// The trait's methods in site.rs: (required, with a default). A required one ends its
/// signature with `;`, a default one opens a body.
fn trait_counts() -> (usize, usize) {
let src = include_str!("../src/site.rs");
let body = src
.split("pub trait BuySite")
.nth(1)
.expect("site.rs declares BuySite");
let body = &body[..body.find("\n}\n").expect("the trait closes")];
let (mut required, mut defaulted) = (0, 0);
let mut sig: Option<String> = None;
for line in body.lines().map(str::trim) {
if line.starts_with("fn ") || line.starts_with("async fn ") {
sig = Some(String::new());
}
if let Some(s) = sig.as_mut() {
s.push_str(line);
if line.ends_with(';') {
required += 1;
sig = None;
} else if line.ends_with('{') {
defaulted += 1;
sig = None;
}
}
}
(required, defaulted)
}
#[test]
fn buysite_is_six_required_and_six_default_methods() {
// 6 required (origin, product_word, who, ticket, held_caps, signup_account) and 6 defaults
// (pages, allowed, trusted_proxies, fixed_buy_url, stepped_up, operator; P4b added the last
// two to A28's "6 + 5", A32-9 deleted grow_needs_checked_tickets). INTEGRATION-v2 says the same.
assert_eq!(trait_counts(), (6, 6));
let doc = include_str!("../../../docs/INTEGRATION-v2.md");
assert!(
doc.contains("6 required + 6 defaults"),
"INTEGRATION-v2 names BuySite's count as the code has it"
);
}