crates/monetize-server/src/services/test_purchase.rs
//! **`TestPurchase`** — the operator's test button over [`crate::test_purchase`].
//! Thin: the operator gate, the blocking pool, the run's view.
use std::sync::Arc;
use monetize_proto as pb;
use monetize_proto::test_purchase_server::TestPurchase;
use tonic::{Request, Response, Status};
use crate::state::AppState;
pub struct TestPurchaseService(pub Arc<AppState>);
/// **The operator's door only.** The interceptor puts the marker on an
/// operator-bearer call and on a control caller (the ONE box's console);
/// anything else — the service bearer a product console holds — is refused.
fn operator<T>(req: &Request<T>) -> Result<(), Status> {
if req.extensions().get::<crate::auth::Operator>().is_some() {
return Ok(());
}
Err(Status::permission_denied(
"the test button is the operator's: the operator bearer, or the console's signed call",
))
}
async fn on_pool<T: Send + 'static>(
state: &Arc<AppState>,
f: impl FnOnce(&Arc<AppState>) -> Result<T, Status> + Send + 'static,
) -> Result<T, Status> {
let state = state.clone();
tokio::task::spawn_blocking(move || f(&state))
.await
.map_err(|e| Status::internal(format!("handler panicked: {e}")))?
}
#[tonic::async_trait]
impl TestPurchase for TestPurchaseService {
async fn actions(
&self,
req: Request<pb::TestActionsRequest>,
) -> Result<Response<pb::TestActionsResponse>, Status> {
operator(&req)?;
let product = req.into_inner().product;
let actions = on_pool(&self.0, move |s| {
Ok(crate::test_purchase::actions(s, product.trim()))
})
.await?;
Ok(Response::new(pb::TestActionsResponse { actions }))
}
async fn start(
&self,
req: Request<pb::TestPurchaseStartRequest>,
) -> Result<Response<pb::TestPurchaseRun>, Status> {
operator(&req)?;
let r = req.into_inner();
let run = on_pool(&self.0, move |s| {
let run =
crate::test_purchase::start(s, r.product.trim(), r.action.trim(), &r.site, r.live)?;
if run.kind == "private_instance" && run.refused.is_empty() {
// Nudged as `EstateOrder.Start` nudges: the order is driven from here on by the tick.
crate::estate_order::tick(s);
}
Ok(crate::test_purchase::view(s, &run))
})
.await?;
Ok(Response::new(run))
}
async fn list(
&self,
req: Request<pb::TestPurchaseListRequest>,
) -> Result<Response<pb::TestPurchaseListResponse>, Status> {
operator(&req)?;
let limit = req.into_inner().limit as usize;
let runs = on_pool(&self.0, move |s| {
Ok(s.tests
.list(limit)
.iter()
.map(|r| crate::test_purchase::view(s, r))
.collect())
})
.await?;
Ok(Response::new(pb::TestPurchaseListResponse { runs }))
}
async fn status(
&self,
req: Request<pb::TestPurchaseStatusRequest>,
) -> Result<Response<pb::TestPurchaseRun>, Status> {
operator(&req)?;
let id = req.into_inner().id;
let run = on_pool(&self.0, move |s| {
let run = s
.tests
.get(id.trim())
.ok_or_else(|| Status::not_found(format!("no test run {id:?}")))?;
Ok(crate::test_purchase::view(s, &run))
})
.await?;
Ok(Response::new(run))
}
}