crates/monetize-ui-server/tests/test_buy_door.rs
//! ★ live 2026-10-14: **the test button is pressed by an admin on the ONE box's console**
//! and monetize refused it — `permission denied` — because the console's plain client
//! carries the SERVICE bearer, which `TestPurchase` refuses. The console's door to the
//! test button is the operator's: its operator bearer when it holds one.
use std::sync::{Arc, Mutex};
use std::time::Duration;
use axum::body::Body;
use http::{Method, Request, StatusCode};
use http_body_util::BodyExt;
use monetize_proto as pb;
use monetize_proto::test_purchase_server::{TestPurchase, TestPurchaseServer};
use monetize_ui_server::app::App;
use monetize_ui_server::grpc::Monetize;
use monetize_ui_server::serve;
use monetize_ui_server::sse::EventHub;
use serde_json::{json, Value};
use tonic::{Request as TReq, Response, Status};
use tower::ServiceExt;
const SERVICE: &str = "the-service-bearer-every-bff-holds";
const OPERATOR: &str = "the-operator-bearer-of-this-console";
const ADMIN: &str = "break-glass-test-buy";
/// The bearer of every call the mock took, by verb. Like monetize, it refuses
/// anything but the operator bearer.
#[derive(Clone, Default)]
struct Mock(Arc<Mutex<Vec<(String, String)>>>);
impl Mock {
fn gate<T>(&self, verb: &str, req: &TReq<T>) -> Result<(), Status> {
let bearer = req.metadata().get("authorization").and_then(|v| v.to_str().ok()).and_then(|s| s.strip_prefix("Bearer ")).unwrap_or("").to_owned();
self.0.lock().unwrap().push((verb.to_owned(), bearer.clone()));
if bearer == OPERATOR {
Ok(())
} else {
Err(Status::permission_denied("the test button is the operator's: the operator bearer, or the console's signed call"))
}
}
}
#[tonic::async_trait]
impl TestPurchase for Mock {
async fn actions(&self, req: TReq<pb::TestActionsRequest>) -> Result<Response<pb::TestActionsResponse>, Status> {
self.gate("Actions", &req)?;
Ok(Response::new(pb::TestActionsResponse { actions: vec![pb::TestActionOffer { product: "gunnar".into(), id: "private-small".into(), label: "Test: private Gunnar small".into(), runnable: true, ..Default::default() }] }))
}
async fn start(&self, req: TReq<pb::TestPurchaseStartRequest>) -> Result<Response<pb::TestPurchaseRun>, Status> {
self.gate("Start", &req)?;
let r = req.into_inner();
Ok(Response::new(pb::TestPurchaseRun { id: "t1".into(), product: r.product, action: r.action, ..Default::default() }))
}
async fn list(&self, req: TReq<pb::TestPurchaseListRequest>) -> Result<Response<pb::TestPurchaseListResponse>, Status> {
self.gate("List", &req)?;
Ok(Response::new(pb::TestPurchaseListResponse { runs: vec![] }))
}
async fn status(&self, req: TReq<pb::TestPurchaseStatusRequest>) -> Result<Response<pb::TestPurchaseRun>, Status> {
self.gate("Status", &req)?;
Err(Status::not_found("no run"))
}
}
async fn call(mock: Mock, with_operator: bool, method: Method, uri: &str, body: Option<Value>) -> (StatusCode, Value) {
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let at = l.local_addr().unwrap().to_string();
tokio::spawn(tonic::transport::Server::builder().add_service(TestPurchaseServer::new(mock)).serve_with_incoming(tonic::transport::server::TcpIncoming::from(l)));
let monetize = Monetize::connect_lazy(&at, SERVICE).unwrap();
let operator = with_operator.then(|| Monetize::connect_lazy(&at, OPERATOR).unwrap());
let app = Arc::new(App { webauthn: None, admin_token: Some(ADMIN.into()), events: EventHub::spawn(monetize.clone(), Duration::from_secs(3600)), monetize, operator, ui_dir: None });
let mut req = Request::builder().method(method).uri(uri).header("authorization", format!("Bearer {ADMIN}"));
let body = match body {
Some(b) => {
req = req.header("content-type", "application/json");
Body::from(b.to_string())
}
None => Body::empty(),
};
let res = serve::router(app).oneshot(req.body(body).unwrap()).await.unwrap();
let status = res.status();
let bytes = res.into_body().collect().await.unwrap().to_bytes();
(status, serde_json::from_slice(&bytes).unwrap_or_else(|_| Value::String(String::from_utf8_lossy(&bytes).into_owned())))
}
/// ★ An admin's read and press of the test button reach monetize as the OPERATOR.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_admins_test_buy_goes_with_the_operator_bearer() {
let mock = Mock::default();
let (status, j) = call(mock.clone(), true, Method::GET, "/api/test-purchase", None).await;
assert_eq!(status, StatusCode::OK, "{j}");
assert_eq!(j["actions"][0]["id"], "private-small", "{j}");
let (status, j) = call(mock.clone(), true, Method::POST, "/api/test-purchase/start", Some(json!({"product": "gunnar", "action": "private-small", "site": "", "live": false}))).await;
assert_eq!(status, StatusCode::OK, "{j}");
assert_eq!(j["id"], "t1", "{j}");
let seen = mock.0.lock().unwrap().clone();
assert!(!seen.is_empty() && seen.iter().all(|(_, b)| b == OPERATOR), "every test-button call carries the operator bearer, never the service one: {seen:?}");
}
/// A console with no operator bearer still calls with its own client (outside an
/// estate that is the console's signed call); here monetize refuses it.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn without_an_operator_bearer_the_consoles_own_client_is_used() {
let mock = Mock::default();
let (status, _) = call(mock.clone(), false, Method::GET, "/api/test-purchase", None).await;
assert_eq!(status, StatusCode::FORBIDDEN);
assert_eq!(mock.0.lock().unwrap()[0].1, SERVICE);
}