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/a16_renewal_offered.rs

//! ★ **A renewal is offered only where monetize verifies the tenant's renewal ticket** (A16,
//! Gunnar's console's `api::subscription::offered`, moved as it is): before Prolong's quote, the
//! renewal and the history, the shop's `Products.List` must say this product checks tenant
//! renewals; otherwise the door answers 501 with Gunnar's sentence and the purchase is never
//! asked. Addresses are `*.mock.invalid`.

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

#[derive(Clone, Default)]
struct Shop {
    renewal: bool,
    asked: Arc<Mutex<u32>>,
}

#[tonic::async_trait]
impl pb::products_server::Products for Shop {
    async fn list(
        &self,
        _: Request<pb::ListProductsRequest>,
    ) -> Result<Response<pb::ListProductsResponse>, Status> {
        Ok(Response::new(pb::ListProductsResponse {
            products: vec![pb::ProductInfo {
                id: "holger".into(),
                tenant_renewal: self.renewal,
                ..Default::default()
            }],
            ..Default::default()
        }))
    }
}

#[tonic::async_trait]
impl pb::purchase_server::Purchase for Shop {
    async fn bundles(
        &self,
        _: Request<pb::ListBundlesRequest>,
    ) -> Result<Response<pb::ListBundlesResponse>, Status> {
        Err(Status::unimplemented("Bundles"))
    }
    async fn start(
        &self,
        _: Request<pb::StartPurchaseRequest>,
    ) -> Result<Response<pb::StartPurchaseResponse>, Status> {
        *self.asked.lock().unwrap() += 1;
        Err(Status::failed_precondition("nothing to renew"))
    }
    async fn status(
        &self,
        _: Request<pb::PurchaseStatusRequest>,
    ) -> Result<Response<pb::PurchaseStatusResponse>, Status> {
        Err(Status::unimplemented("Status"))
    }
    async fn quote(
        &self,
        _: Request<pb::QuoteRequest>,
    ) -> Result<Response<pb::QuoteResponse>, Status> {
        *self.asked.lock().unwrap() += 1;
        Ok(Response::new(pb::QuoteResponse::default()))
    }
    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> {
        *self.asked.lock().unwrap() += 1;
        Ok(Response::new(pb::PurchaseHistoryResponse::default()))
    }
}

struct Site;

impl BuySite for Site {
    fn origin(&self) -> String {
        "https://holger.mock.invalid".into()
    }
    fn product_word(&self) -> String {
        "Holger".into()
    }
    async fn who(&self, _: &HeaderMap) -> Result<Who, Refusal> {
        Ok(Who::Person(Buyer {
            account: "x14".into(),
            ..Default::default()
        }))
    }
    async fn ticket(
        &self,
        _: &HeaderMap,
        _: &Buyer,
        purpose: &str,
        _: &BTreeMap<String, u64>,
    ) -> Result<Vec<u8>, Refusal> {
        Ok(purpose.as_bytes().to_vec())
    }
    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)
    }
}

async fn relay(renewal: bool) -> (Arc<Relay<Site>>, Shop) {
    let shop = Shop {
        renewal,
        ..Shop::default()
    };
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
    let at = listener.local_addr().unwrap();
    tokio::spawn(
        tonic::transport::Server::builder()
            .add_service(pb::products_server::ProductsServer::new(shop.clone()))
            .add_service(pb::purchase_server::PurchaseServer::new(shop.clone()))
            .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener)),
    );
    let link = ShopLink::connect_lazy(
        &Door {
            url: format!("http://{at}"),
            ..Door::default()
        },
        "site-bearer-for-tests",
        "holger",
    )
    .unwrap();
    (Arc::new(Relay::new(Site, link)), shop)
}

async fn call(r: &Arc<Relay<Site>>, method: &str, uri: &str) -> (StatusCode, serde_json::Value) {
    use tower::ServiceExt;
    let req = axum::http::Request::builder()
        .method(method)
        .uri(uri)
        .header("content-type", "application/json")
        .body(axum::body::Body::from(if method == "POST" {
            "{}"
        } else {
            ""
        }))
        .unwrap();
    let res = monetize_site_buy::router(r.clone())
        .oneshot(req)
        .await
        .unwrap();
    let status = res.status();
    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),
    )
}

#[tokio::test]
async fn no_renewal_where_the_shop_does_not_verify_renewal_tickets() {
    let (r, shop) = relay(false).await;
    for (m, uri) in [
        ("GET", "/api/account/subscription"),
        ("POST", "/api/account/subscription/renew"),
        ("GET", "/api/account/subscription/history"),
    ] {
        let (st, body) = call(&r, m, uri).await;
        assert_eq!(st, StatusCode::NOT_IMPLEMENTED, "{uri}: {body}");
        assert_eq!(
            body["error"],
            monetize_site_buy::relay::RENEWAL_NOT_VERIFIED,
            "{uri}"
        );
    }
    assert_eq!(
        *shop.asked.lock().unwrap(),
        0,
        "the purchase is never asked"
    );
}

#[tokio::test]
async fn a_renewal_goes_on_where_the_shop_verifies_them() {
    let (r, shop) = relay(true).await;
    assert_eq!(
        call(&r, "GET", "/api/account/subscription").await.0,
        StatusCode::OK
    );
    assert_eq!(
        call(&r, "GET", "/api/account/subscription/history").await.0,
        StatusCode::OK
    );
    assert_eq!(*shop.asked.lock().unwrap(), 2);
}