Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

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
            }
        }
    )*};
}