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/common/mod.rs

//! A fake ONE monetize (Signup, EstateOrder, Purchase over monetize-proto, on a loopback
//! port) and a fake product site, for the relay's tests. Addresses are `*.mock.invalid`.

#![allow(dead_code)]

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, Member, Refusal, Relay, ShopLink, Who};
use tonic::{Request, Response, Status};

pub const BEARER: &str = "site-bearer-for-tests";
pub const ORIGIN: &str = "https://holger.mock.invalid";

/// What the fake heard: every request, and the bearer it came with.
#[derive(Default)]
pub struct Heard {
    pub bearers: Vec<String>,
    pub signup_start: Option<pb::StartSignupRequest>,
    pub estate_start: Option<pb::EstateOrderStartRequest>,
    pub change: Option<pb::EstateOrderChangePlanRequest>,
    pub status_ticket: Option<Vec<u8>>,
    pub purchase_start: Option<pb::StartPurchaseRequest>,
    pub bundles: Option<pb::ListBundlesRequest>,
    pub sign_in_again: Option<pb::SignupSignInAgainRequest>,
    pub purchases: Option<pb::EstateOrderPurchasesRequest>,
}

#[derive(Clone, Default)]
pub struct Fake {
    pub heard: Arc<Mutex<Heard>>,
    /// The username `Signup.Status` answers CREATED for.
    pub created_user: Arc<Mutex<String>>,
    pub product: String,
}

impl Fake {
    fn hear<T>(&self, r: &Request<T>) {
        let b = r
            .metadata()
            .get("authorization")
            .and_then(|v| v.to_str().ok())
            .unwrap_or("")
            .to_owned();
        self.heard.lock().unwrap().bearers.push(b);
    }
}

#[tonic::async_trait]
impl pb::signup_server::Signup for Fake {
    async fn status(
        &self,
        r: Request<pb::SignupStatusRequest>,
    ) -> Result<Response<pb::SignupStatusResponse>, Status> {
        self.hear(&r);
        let user = self.created_user.lock().unwrap().clone();
        if user.is_empty() {
            return Ok(Response::new(pb::SignupStatusResponse {
                state: pb::signup_status_response::State::Pending as i32,
                words: "Waiting for your payment to be confirmed. Nothing more is needed from you."
                    .into(),
                reason: "operator words".into(),
                ..Default::default()
            }));
        }
        Ok(Response::new(pb::SignupStatusResponse {
            state: pb::signup_status_response::State::Created as i32,
            username: user,
            product: self.product.clone(),
            ..Default::default()
        }))
    }
    async fn check_name(
        &self,
        r: Request<pb::CheckNameRequest>,
    ) -> Result<Response<pb::CheckNameResponse>, Status> {
        self.hear(&r);
        let r = r.into_inner();
        let free = r.username != "taken";
        Ok(Response::new(pb::CheckNameResponse {
            free,
            reason: if free {
                String::new()
            } else {
                format!("username {:?} is taken; choose another", r.username)
            },
        }))
    }
    async fn start(
        &self,
        r: Request<pb::StartSignupRequest>,
    ) -> Result<Response<pb::StartSignupResponse>, Status> {
        self.hear(&r);
        let r = r.into_inner();
        if r.username == "nosink" {
            return Err(Status::unimplemented("no checkout here"));
        }
        if r.username == "held" {
            return Err(Status::failed_precondition("Someone is signing up with this account name right now. Choose another name, or try again in half an hour."));
        }
        self.heard.lock().unwrap().signup_start = Some(r);
        Ok(Response::new(pb::StartSignupResponse {
            url: "https://checkout.mock.invalid/c/pay/cs_test_abcdefghijkl".into(),
            session: "cs_test_abcdefghijkl".into(),
        }))
    }
    async fn rename(
        &self,
        r: Request<pb::RenameSignupRequest>,
    ) -> Result<Response<pb::RenameSignupResponse>, Status> {
        self.hear(&r);
        Err(Status::not_found("no such signup"))
    }
    async fn plans(
        &self,
        r: Request<pb::SignupPlansRequest>,
    ) -> Result<Response<pb::SignupPlansResponse>, Status> {
        self.hear(&r);
        let market = r.into_inner().market;
        Ok(Response::new(pb::SignupPlansResponse {
            plans: vec![
                pb::SignupPlanOffer {
                    id: format!("{}/account", self.product),
                    name: "Free account".into(),
                    price_words: "Free".into(),
                    ..Default::default()
                },
                pb::SignupPlanOffer {
                    id: format!("{}/5gb-1m", self.product),
                    name: "5 GB".into(),
                    price_words: "100.00 SEK a month".into(),
                    period_days: 30,
                    ..Default::default()
                },
            ],
            market,
        }))
    }
    async fn send_sign_in_again(
        &self,
        r: Request<pb::SignupSignInAgainRequest>,
    ) -> Result<Response<pb::SignupSignInAgainResponse>, Status> {
        self.hear(&r);
        self.heard.lock().unwrap().sign_in_again = Some(r.into_inner());
        Ok(Response::new(pb::SignupSignInAgainResponse {
            words: "We’ve sent it again to o•••@m•••.invalid.".into(),
        }))
    }
}

#[tonic::async_trait]
impl pb::estate_order_server::EstateOrder for Fake {
    async fn product_order(
        &self,
        _: tonic::Request<pb::EstateProductOrderRequest>,
    ) -> Result<tonic::Response<pb::EstateProductOrderResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("product_order"))
    }
    async fn product_order_answer(
        &self,
        _: tonic::Request<pb::EstateProductOrderAnswerRequest>,
    ) -> Result<tonic::Response<pb::EstateProductOrderAnswerResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("product_order_answer"))
    }
    async fn product_order_kinds(
        &self,
        _: tonic::Request<pb::EstateProductOrderKindsRequest>,
    ) -> Result<tonic::Response<pb::EstateProductOrderKindsResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("product_order_kinds"))
    }
    async fn start(
        &self,
        r: Request<pb::EstateOrderStartRequest>,
    ) -> Result<Response<pb::EstateOrderStartResponse>, Status> {
        self.hear(&r);
        let r = r.into_inner();
        if r.company == "taken" {
            return Err(Status::already_exists("taken.holger.mock.invalid is taken"));
        }
        self.heard.lock().unwrap().estate_start = Some(r.clone());
        Ok(Response::new(pb::EstateOrderStartResponse {
            reference: format!("estate/{}", r.company),
            checkout_url: "https://checkout.mock.invalid/c/pay/cs_test_estate".into(),
            state: "waiting_for_payment".into(),
            words: "Waiting for your payment on the payment page.".into(),
            price_words: "385.20 EUR".into(),
        }))
    }
    async fn status(
        &self,
        r: Request<pb::EstateOrderStatusRequest>,
    ) -> Result<Response<pb::EstateOrderStatusResponse>, Status> {
        self.hear(&r);
        let r = r.into_inner();
        self.heard.lock().unwrap().status_ticket = Some(r.actor_ticket.clone());
        Ok(Response::new(pb::EstateOrderStatusResponse {
            company: r.company,
            state: "queued".into(),
            words: "Your Holger is paid and being prepared.".into(),
            ..Default::default()
        }))
    }
    async fn send_sign_in_again(
        &self,
        r: Request<pb::EstateOrderSignInAgainRequest>,
    ) -> Result<Response<pb::EstateOrderSignInAgainResponse>, Status> {
        self.hear(&r);
        Ok(Response::new(pb::EstateOrderSignInAgainResponse {
            words: "sent".into(),
        }))
    }
    async fn change_plan(
        &self,
        r: Request<pb::EstateOrderChangePlanRequest>,
    ) -> Result<Response<pb::EstateOrderChangePlanResponse>, Status> {
        self.hear(&r);
        let r = r.into_inner();
        self.heard.lock().unwrap().change = Some(r.clone());
        Ok(Response::new(pb::EstateOrderChangePlanResponse {
            reference: format!("estate/{}/change-1", r.company),
            checkout_url: "https://checkout.mock.invalid/c/pay/cs_test_change".into(),
            status: "ordered".into(),
            words: "held now".into(),
            price_words: "12.00 EUR".into(),
            twin_plan: String::new(),
        }))
    }
    async fn ladder(
        &self,
        r: Request<pb::EstateOrderLadderRequest>,
    ) -> Result<Response<pb::EstateOrderLadderResponse>, Status> {
        self.hear(&r);
        Ok(Response::new(pb::EstateOrderLadderResponse {
            steps: vec![pb::EstateLadderStep {
                name: "Mini".into(),
                sizes: Some(pb::EstateSizes {
                    front_plan: "f".into(),
                    main_plan: "m".into(),
                    main_data_gib: 40,
                    ..Default::default()
                }),
                monthly_words: "96.20 EUR a month".into(),
                available: true,
                ..Default::default()
            }],
            apex: "holger.mock.invalid".into(),
            ..Default::default()
        }))
    }
    async fn sells(
        &self,
        r: Request<pb::EstateOrderSellsRequest>,
    ) -> Result<Response<pb::EstateOrderSellsResponse>, Status> {
        self.hear(&r);
        Ok(Response::new(pb::EstateOrderSellsResponse {
            estates: true,
        }))
    }
    async fn renew(
        &self,
        _: Request<pb::EstateOrderRenewRequest>,
    ) -> Result<Response<pb::EstateOrderRenewResponse>, Status> {
        Err(Status::unimplemented("Renew"))
    }
    async fn purchases(
        &self,
        r: Request<pb::EstateOrderPurchasesRequest>,
    ) -> Result<Response<pb::EstateOrderPurchasesResponse>, Status> {
        self.hear(&r);
        let r = r.into_inner();
        self.heard.lock().unwrap().purchases = Some(r.clone());
        if r.account != "x14" {
            return Ok(Response::new(pb::EstateOrderPurchasesResponse::default()));
        }
        Ok(Response::new(pb::EstateOrderPurchasesResponse {
            estates: vec![pb::EstatePurchase {
                company: "acme".into(),
                host: "acme.holger.mock.invalid".into(),
                url: "https://acme.holger.mock.invalid".into(),
                state: "ready".into(),
                step: "Mini".into(),
                ..Default::default()
            }],
            rooms: vec![],
            account_here: true,
        }))
    }
    async fn notice(
        &self,
        _: Request<pb::EstateNoticeRequest>,
    ) -> Result<Response<pb::EstateNoticeResponse>, Status> {
        Err(Status::unimplemented("Notice"))
    }
    async fn cancel(
        &self,
        _: Request<pb::EstateOrderCancelRequest>,
    ) -> Result<Response<pb::EstateOrderCancelResponse>, Status> {
        Err(Status::unimplemented("Cancel"))
    }
    async fn settle(
        &self,
        _: Request<pb::EstateOrderSettleRequest>,
    ) -> Result<Response<pb::EstateOrderSettleResponse>, Status> {
        Err(Status::unimplemented("Settle"))
    }
    async fn restore_front(
        &self,
        _: Request<pb::EstateOrderRestoreFrontRequest>,
    ) -> Result<Response<pb::EstateOrderRestoreFrontResponse>, Status> {
        Err(Status::unimplemented("RestoreFront"))
    }
    async fn terminate(
        &self,
        _: Request<pb::EstateOrderTerminateRequest>,
    ) -> Result<Response<pb::EstateOrderTerminateResponse>, Status> {
        Err(Status::unimplemented("Terminate"))
    }
    async fn first_link(
        &self,
        _: Request<pb::EstateOrderFirstLinkRequest>,
    ) -> Result<Response<pb::EstateOrderFirstLinkResponse>, Status> {
        Err(Status::unimplemented("FirstLink"))
    }
    async fn refeed(
        &self,
        _: Request<pb::EstateOrderRefeedRequest>,
    ) -> Result<Response<pb::EstateOrderRefeedResponse>, Status> {
        Err(Status::unimplemented("Refeed"))
    }
    async fn rebuild_main(
        &self,
        _: Request<pb::EstateRebuildMainRequest>,
    ) -> Result<Response<pb::EstateRebuildMainResponse>, Status> {
        Err(Status::unimplemented("RebuildMain"))
    }
    async fn order_site(
        &self,
        _: Request<pb::EstateOrderSiteRequest>,
    ) -> Result<Response<pb::EstateOrderSiteResponse>, Status> {
        Err(Status::unimplemented("OrderSite"))
    }
    async fn list_sites(
        &self,
        _: Request<pb::EstateSitesRequest>,
    ) -> Result<Response<pb::EstateSitesResponse>, Status> {
        Err(Status::unimplemented("ListSites"))
    }
    async fn update_estate(
        &self,
        _: Request<pb::EstateUpdateRequest>,
    ) -> Result<Response<pb::EstateUpdateResponse>, Status> {
        Err(Status::unimplemented("UpdateEstate"))
    }
    async fn update_status(
        &self,
        _: Request<pb::EstateUpdateStatusRequest>,
    ) -> Result<Response<pb::EstateUpdateResponse>, Status> {
        Err(Status::unimplemented("UpdateStatus"))
    }
    async fn cancel_update(
        &self,
        _: Request<pb::EstateUpdateStatusRequest>,
    ) -> Result<Response<pb::EstateUpdateResponse>, Status> {
        Err(Status::unimplemented("cancel_update"))
    }
    async fn set_live_update(
        &self,
        _: Request<pb::EstateSetLiveUpdateRequest>,
    ) -> Result<Response<pb::EstateUpdateResponse>, Status> {
        Err(Status::unimplemented("SetLiveUpdate"))
    }
    async fn adopt_estate(
        &self,
        _: Request<pb::EstateAdoptRequest>,
    ) -> Result<Response<pb::EstateAdoptResponse>, Status> {
        Err(Status::unimplemented("AdoptEstate"))
    }
    async fn list_code_red(
        &self,
        _: Request<pb::CodeRedListRequest>,
    ) -> Result<Response<pb::CodeRedList>, Status> {
        Err(Status::unimplemented("ListCodeRed"))
    }
    async fn ack_code_red(
        &self,
        _: Request<pb::CodeRedAckRequest>,
    ) -> Result<Response<pb::CodeRedList>, Status> {
        Err(Status::unimplemented("AckCodeRed"))
    }
    async fn raise_code_red(
        &self,
        _: Request<pb::CodeRed>,
    ) -> Result<Response<pb::CodeRedList>, Status> {
        Err(Status::unimplemented("RaiseCodeRed"))
    }
    async fn clear_code_red(
        &self,
        _: Request<pb::CodeRedClearRequest>,
    ) -> Result<Response<pb::CodeRedList>, Status> {
        Err(Status::unimplemented("ClearCodeRed"))
    }
    async fn narrate_log(
        &self,
        _: Request<pb::EstateNarrateLogRequest>,
    ) -> Result<Response<pb::EstateNarrateLogResponse>, Status> {
        Err(Status::unimplemented("NarrateLog"))
    }
    async fn install_log(
        &self,
        _: Request<pb::EstateInstallLogRequest>,
    ) -> Result<Response<pb::EstateInstallLogResponse>, Status> {
        Err(Status::unimplemented("InstallLog"))
    }
}

#[tonic::async_trait]
impl pb::products_server::Products for Fake {
    /// A32-9: every site's grow doors ask first whether the shop checks tenant tickets: it does.
    async fn list(
        &self,
        _: Request<pb::ListProductsRequest>,
    ) -> Result<Response<pb::ListProductsResponse>, Status> {
        Ok(Response::new(pb::ListProductsResponse {
            products: vec![pb::ProductInfo {
                id: self.product.clone(),
                tenant_renewal: true,
                ..Default::default()
            }],
            ..Default::default()
        }))
    }
}

#[tonic::async_trait]
impl pb::purchase_server::Purchase for Fake {
    async fn bundles(
        &self,
        r: Request<pb::ListBundlesRequest>,
    ) -> Result<Response<pb::ListBundlesResponse>, Status> {
        self.hear(&r);
        self.heard.lock().unwrap().bundles = Some(r.into_inner());
        Ok(Response::new(pb::ListBundlesResponse {
            bundles: vec![
                pb::Bundle {
                    id: "more10".into(),
                    label: "10.0 GiB more room".into(),
                    price: "220.00 SEK".into(),
                    period: "month".into(),
                    delta: [("disk_bytes".to_string(), 10u64)].into_iter().collect(),
                    can_serve: true,
                    ..Default::default()
                },
                pb::Bundle {
                    id: "huge".into(),
                    label: "1 TiB more room".into(),
                    can_serve: false,
                    reason: "no room".into(),
                    ..Default::default()
                },
            ],
            reading: false,
            plan: Some(pb::PlanStatus {
                state: "none".into(),
                ..Default::default()
            }),
        }))
    }
    async fn start(
        &self,
        r: Request<pb::StartPurchaseRequest>,
    ) -> Result<Response<pb::StartPurchaseResponse>, Status> {
        self.hear(&r);
        self.heard.lock().unwrap().purchase_start = Some(r.into_inner());
        Ok(Response::new(pb::StartPurchaseResponse {
            reference: "p-1".into(),
            state: pb::PurchaseState::Pending as i32,
            redirect_url: "https://checkout.mock.invalid/c/pay/cs_test_room".into(),
            ..Default::default()
        }))
    }
    async fn status(
        &self,
        r: Request<pb::PurchaseStatusRequest>,
    ) -> Result<Response<pb::PurchaseStatusResponse>, Status> {
        self.hear(&r);
        let r = r.into_inner();
        Ok(Response::new(pb::PurchaseStatusResponse {
            reference: r.reference,
            state: pb::PurchaseState::Paid as i32,
            phase: pb::PurchasePhase::Done as i32,
            ..Default::default()
        }))
    }
    async fn quote(
        &self,
        _: Request<pb::QuoteRequest>,
    ) -> Result<Response<pb::QuoteResponse>, Status> {
        Err(Status::unimplemented("Quote"))
    }
    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> {
        Err(Status::unimplemented("History"))
    }
}

/// **The fake product site**: `x-test-user` names who asks (absent: a visitor; `machine`;
/// `member:<account>`; else the account).
#[derive(Clone)]
pub struct Site {
    pub word: String,
    /// The site's fixed buy link, where the shop mints no checkout.
    pub fixed: Option<String>,
}

impl BuySite for Site {
    fn origin(&self) -> String {
        ORIGIN.to_owned()
    }
    fn product_word(&self) -> String {
        self.word.clone()
    }
    fn fixed_buy_url(&self) -> Option<String> {
        self.fixed.clone()
    }
    async fn who(&self, headers: &HeaderMap) -> Result<Who, Refusal> {
        Ok(
            match headers.get("x-test-user").and_then(|v| v.to_str().ok()) {
                None => Who::Visitor,
                Some("machine") => Who::Machine,
                Some("down") => {
                    return Err(Refusal::new(
                        StatusCode::SERVICE_UNAVAILABLE,
                        "Holger cannot be reached right now: try again in a minute.",
                    ))
                }
                Some(m) if m.starts_with("member:") => Who::Person(Buyer {
                    account: "ann".into(),
                    member: Some(Member {
                        account: m[7..].into(),
                        admin: "boss".into(),
                        line: format!("You are a member of {}.", &m[7..]),
                    }),
                    ..Default::default()
                }),
                Some(a) => Who::Person(Buyer {
                    account: a.into(),
                    ..Default::default()
                }),
            },
        )
    }
    async fn ticket(
        &self,
        _: &HeaderMap,
        buyer: &Buyer,
        purpose: &str,
        caps: &BTreeMap<String, u64>,
    ) -> Result<Vec<u8>, Refusal> {
        Ok(format!("{purpose}:{}:{caps:?}", buyer.account).into_bytes())
    }
    async fn held_caps(&self, _: &HeaderMap, _: &Buyer) -> Result<BTreeMap<String, u64>, Refusal> {
        Ok(BTreeMap::from([("disk_bytes".to_string(), 5u64)]))
    }
    async fn signup_account(&self, user: &str) -> Result<Account, Refusal> {
        match user {
            "done" => Ok(Account::HasPasskey),
            "x14" => Ok(Account::NoPasskey),
            _ => Err(Refusal::new(StatusCode::CONFLICT, "not here yet")),
        }
    }
}

/// A fake monetize on a loopback port and a relay to it.
pub async fn relay(product: &str) -> (Arc<Relay<Site>>, Fake) {
    relay_with(product, None).await
}

/// [`relay`] for a site with a fixed buy link.
pub async fn relay_with(product: &str, fixed: Option<&str>) -> (Arc<Relay<Site>>, Fake) {
    let fake = Fake {
        product: product.into(),
        ..Fake::default()
    };
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
    let at = listener.local_addr().unwrap();
    let serve = tonic::transport::Server::builder()
        .add_service(pb::signup_server::SignupServer::new(fake.clone()))
        .add_service(pb::estate_order_server::EstateOrderServer::new(
            fake.clone(),
        ))
        .add_service(pb::purchase_server::PurchaseServer::new(fake.clone()))
        .add_service(pb::products_server::ProductsServer::new(fake.clone()))
        .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener));
    tokio::spawn(serve);
    let shop = ShopLink::connect_lazy(
        &Door {
            url: format!("http://{at}"),
            ..Door::default()
        },
        BEARER,
        product,
    )
    .unwrap();
    (
        Arc::new(Relay::new(
            Site {
                word: "Holger".into(),
                fixed: fixed.map(str::to_owned),
            },
            shop,
        )),
        fake,
    )
}

/// One request through the relay's router: `(status, json body, location)`.
pub async fn call(
    r: &Arc<Relay<Site>>,
    method: &str,
    uri: &str,
    user: Option<&str>,
    body: Option<serde_json::Value>,
) -> (StatusCode, serde_json::Value, Option<String>) {
    use tower::ServiceExt;
    let mut req = axum::http::Request::builder()
        .method(method)
        .uri(uri)
        .header("accept-language", "sv-SE");
    if let Some(u) = user {
        req = req.header("x-test-user", u);
    }
    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(r.clone())
        .oneshot(req)
        .await
        .unwrap();
    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();
    (
        status,
        serde_json::from_slice(&bytes).unwrap_or(serde_json::Value::Null),
        location,
    )
}