crates/monetize-site-buy/tests/sites/mod.rs
//! **Two product sites over one relay**: a Gunnar-shaped and a Holger-shaped [`BuySite`], each
//! with its own origin, pages, session (Gunnar's cookie, Holger's bearer), tickets and budget,
//! over the crate's fake ONE box. Every scenario runs for both: one flow, only the provider
//! differs. Addresses are `*.mock.invalid`.
#![allow(dead_code)]
use std::sync::{Arc, Mutex};
use axum::http::StatusCode;
use monetize_site_buy::testkit::{self, FakeOneBox, STRIPE};
use monetize_site_buy::Relay;
use serde_json::Value;
pub const BEARER: &str = "site-bearer-for-tests";
pub use monetize_site_buy::testkit::{Shape, TestSite};
/// One answer of the relay.
#[derive(Debug)]
pub struct Reply {
pub status: StatusCode,
pub body: Value,
pub location: Option<String>,
}
impl Reply {
pub fn error(&self) -> &str {
self.body["error"].as_str().unwrap_or("")
}
}
/// **A site, its relay and the ONE box behind it.**
pub struct Kit {
pub shape: Shape,
pub fake: FakeOneBox,
pub site: TestSite,
pub relay: Arc<Relay<TestSite>>,
/// Every address the relay handed a browser (bodies' URLs, `Location`s).
pub emitted: Mutex<Vec<String>>,
}
impl Kit {
pub async fn new(shape: Shape) -> Kit {
Kit::with_budget(shape, None).await
}
pub async fn with_budget(shape: Shape, budget: Option<u32>) -> Kit {
let fake = FakeOneBox::spawn(shape.product(), BEARER).await;
let link = fake.link();
Kit::over(shape, fake, link, budget)
}
/// A kit whose link goes to `link` (a box that is down), its fake standing by.
pub fn over(
shape: Shape,
fake: FakeOneBox,
link: monetize_site_buy::ShopLink,
budget: Option<u32>,
) -> Kit {
let site = TestSite::over(shape, &fake).with_budget(budget);
let relay = Arc::new(Relay::new(site.clone(), link));
Kit {
shape,
fake,
site,
relay,
emitted: Mutex::default(),
}
}
pub fn origin(&self) -> &'static str {
self.shape.origin()
}
/// One request through the relay's router, as `who` (`None`: a visitor).
pub async fn ask(
&self,
method: &str,
uri: &str,
who: Option<&str>,
body: Option<Value>,
) -> Reply {
use tower::ServiceExt;
let mut req = axum::http::Request::builder()
.method(method)
.uri(uri)
.header("accept-language", "sv-SE")
.header("x-forwarded-for", "203.0.113.7");
if let Some(u) = who {
let (name, value) = self.shape.session(u);
req = req.header(name, value);
}
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(self.relay.clone())
.oneshot(req)
.await
.expect("the relay answers every request");
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();
let body: Value = serde_json::from_slice(&bytes).unwrap_or(Value::Null);
let mut emitted = self.emitted.lock().unwrap();
emitted.extend(location.clone());
collect_urls(&body, &mut emitted);
Reply {
status,
body,
location,
}
}
/// The checkout the relay sent the browser to (one Stripe page), paid at the fake vendor.
pub fn pay(&self, url: &str) {
assert!(
url.starts_with(STRIPE),
"the browser is sent to the vendor's page and nowhere else: {url}"
);
assert!(self.fake.pay(url), "the fake vendor knows {url}");
}
/// ★ **The owner's buy rules where the relay decides them**, over everything this kit did:
/// every address the relay handed out or gave the shop is on the site (or the vendor's
/// page), never on the shop's own; an e-mail address rides only on a call that makes an
/// account; a sign-in mail goes only to an account made here.
pub fn assert_buy_rules(&self) {
let origin = self.origin();
for url in self.emitted.lock().unwrap().iter() {
assert!(
!url.contains("monetize.") && !url.contains("ignalina"),
"the relay handed a browser the shop's address: {url}"
);
let on_site = url.starts_with('/') && !url.starts_with("//")
|| url.starts_with(&format!("{origin}/"));
let vendor = url.starts_with(&format!("{STRIPE}/"));
let instance = url.starts_with("https://") && url.contains(testkit::APEX);
assert!(on_site || vendor || instance, "an address that is neither the site's, the vendor's page nor the bought instance: {url}");
}
let b = self.fake.book();
for (call, url) in &b.urls {
assert!(
url.starts_with(&format!("{origin}/")),
"{call} carried a way back that is not on {origin}: {url}"
);
}
for c in &b.checkouts {
if !c.success_url.is_empty() {
assert!(
c.return_url()
.starts_with(&format!("{origin}/welcome?session=cs_test_")),
"the vendor's way back is the site's welcome: {}",
c.return_url()
);
}
}
for (call, email) in &b.emails {
if email.is_empty() {
continue;
}
let makes_account = call == "Signup.Start" || b.checkouts.iter().any(|c| matches!(&c.what, testkit::What::Estate { new_account: true, email: e, .. } if e == email));
assert!(
makes_account,
"{call} carried an e-mail address ({email}) without making an account"
);
}
let mut made: Vec<&String> = b
.checkouts
.iter()
.filter(|c| c.paid)
.filter_map(|c| match &c.what {
testkit::What::Signup { email, .. } => Some(email),
testkit::What::Estate {
new_account: true,
email,
..
} => Some(email),
_ => None,
})
.collect();
let mut sign_in: Vec<&String> = b
.mails
.iter()
.filter(|m| m.kind == "sign-in")
.map(|m| &m.to)
.collect();
made.sort();
sign_in.sort();
assert_eq!(
sign_in, made,
"a sign-in mail goes once to each account made, and to nobody else"
);
for m in &b.mails {
assert!(m.to.ends_with("@mock.invalid"), "{m:?}");
assert!(
m.link.starts_with(&format!("{origin}/")),
"a mailed link is on the site: {m:?}"
);
}
}
}
fn collect_urls(v: &Value, out: &mut Vec<String>) {
match v {
Value::String(s)
if s.starts_with('/') || s.starts_with("http://") || s.starts_with("https://") =>
{
out.push(s.clone())
}
Value::Array(a) => a.iter().for_each(|x| collect_urls(x, out)),
Value::Object(o) => o.values().for_each(|x| collect_urls(x, out)),
_ => {}
}
}
/// `scenario!(name)`: the async fn `name(Shape)` as two tests, `name::gunnar` and `name::holger`.
#[macro_export]
macro_rules! for_both_sites {
($($name:ident),* $(,)?) => {$(
mod $name {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn gunnar() {
super::$name($crate::sites::Shape::Gunnar).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn holger() {
super::$name($crate::sites::Shape::Holger).await
}
}
)*};
}