use super::*;
fn paying_app(db_url: &str) -> String {
format!(
r#"
[server]
base_path = "/api"
public_url = "https://example.test"
[database]
url = "{db_url}"
[payments]
provider = "stripe"
secret_key = "sk_test_never_used"
publishable_key = "pk_test_public"
webhook_secret = "whsec_test"
currency = "eur"
"#
)
}
fn plain_app(db_url: &str) -> String {
format!("[server]\nbase_path = \"/api\"\n\n[database]\nurl = \"{db_url}\"\n")
}
#[ntex::test]
async fn without_a_provider_there_is_no_billing_at_all() {
let db = TempDatabase::create("nopay").await;
let root = temp_dir("nopay");
write_files(&root, &[("main.toml", &plain_app(&db.url))]);
let state = load_state(&root).await;
assert!(!state.payments_enabled());
for resource in [
"billing_product",
"billing_price",
"billing_customer",
"billing_subscription",
"billing_payment",
"billing_event",
] {
assert!(
state.app.resources.get(resource).is_none(),
"`{resource}` exists in an app that takes no money"
);
}
let app = init_http_app!(state);
for path in [
"/api/billing/checkout",
"/api/billing/portal",
"/api/billing/webhook",
] {
let resp = test::call_service(&app, req_json("POST", path, json!({}))).await;
assert!(
matches!(resp.status().as_u16(), 404 | 405),
"{path} was handled by a server that takes no payments"
);
}
db.cleanup().await;
fs::remove_dir_all(root).unwrap();
}
#[ntex::test]
async fn a_provider_brings_the_billing_resources_and_routes() {
let db = TempDatabase::create("pay").await;
let root = temp_dir("pay");
write_files(&root, &[("main.toml", &paying_app(&db.url))]);
let state = load_state(&root).await;
assert!(state.payments_enabled());
for resource in ["billing_product", "billing_price", "billing_subscription"] {
assert!(state.app.resources.get(resource).is_some(), "{resource}");
}
let app = init_http_app!(state);
let resp = test::call_service(
&app,
test::TestRequest::get()
.uri("/api/billing_price")
.to_request(),
)
.await;
assert_eq!(resp.status().as_u16(), 200);
db.cleanup().await;
fs::remove_dir_all(root).unwrap();
}
#[ntex::test]
async fn the_public_config_carries_the_publishable_key_and_not_the_secret() {
let db = TempDatabase::create("payconf").await;
let root = temp_dir("payconf");
write_files(&root, &[("main.toml", &paying_app(&db.url))]);
let app = init_http_app!(load_state(&root).await);
let resp = test::call_service(
&app,
test::TestRequest::get()
.uri("/api/billing/config")
.to_request(),
)
.await;
assert_eq!(resp.status().as_u16(), 200);
let body = read_json(resp).await;
assert_eq!(body["provider"], "stripe");
assert_eq!(body["publishable_key"], "pk_test_public");
assert_eq!(body["currency"], "eur");
assert_eq!(body["webhooks_configured"], true);
assert!(!body.to_string().contains("sk_test"), "{body}");
db.cleanup().await;
fs::remove_dir_all(root).unwrap();
}
#[ntex::test]
async fn only_an_admin_of_a_named_organization_can_start_a_checkout() {
let db = TempDatabase::create("paycheckout").await;
let root = temp_dir("paycheckout");
write_files(&root, &[("main.toml", &paying_app(&db.url))]);
let state = load_state(&root).await;
let (member_token, org) = member_with_role(&state, "member@example.test", "member").await;
let app = init_http_app!(state);
let body = json!({ "price_id": Uuid::new_v4().to_string() });
let resp = test::call_service(
&app,
req_json("POST", "/api/billing/checkout", body.clone()),
)
.await;
assert_eq!(resp.status().as_u16(), 401);
let resp = test::call_service(
&app,
bearer(
test::TestRequest::post()
.uri("/api/billing/checkout")
.header(CONTENT_TYPE, "application/json")
.set_payload(body.to_string()),
&member_token,
)
.to_request(),
)
.await;
assert_eq!(resp.status().as_u16(), 400);
let resp = test::call_service(
&app,
bearer(
test::TestRequest::post()
.uri("/api/billing/checkout")
.header(CONTENT_TYPE, "application/json")
.header("x-organization", org.as_str())
.set_payload(body.to_string()),
&member_token,
)
.to_request(),
)
.await;
assert_eq!(resp.status().as_u16(), 403);
db.cleanup().await;
fs::remove_dir_all(root).unwrap();
}
#[ntex::test]
async fn an_admin_buying_an_unknown_price_gets_a_404_and_no_provider_call() {
let db = TempDatabase::create("payprice").await;
let root = temp_dir("payprice");
write_files(&root, &[("main.toml", &paying_app(&db.url))]);
let state = load_state(&root).await;
let (token, org) = member_with_role(&state, "boss@example.test", "admin").await;
let app = init_http_app!(state);
for (price_id, expected) in [
("nonsense", 422),
(Uuid::new_v4().to_string().as_str(), 404),
] {
let resp = test::call_service(
&app,
bearer(
test::TestRequest::post()
.uri("/api/billing/checkout")
.header(CONTENT_TYPE, "application/json")
.header("x-organization", org.as_str())
.set_payload(json!({ "price_id": price_id }).to_string()),
&token,
)
.to_request(),
)
.await;
assert_eq!(resp.status().as_u16(), expected, "price_id {price_id}");
}
db.cleanup().await;
fs::remove_dir_all(root).unwrap();
}
#[ntex::test]
async fn an_unsigned_webhook_is_refused_and_records_nothing() {
let db = TempDatabase::create("payhook").await;
let root = temp_dir("payhook");
write_files(&root, &[("main.toml", &paying_app(&db.url))]);
let state = load_state(&root).await;
let events = state.table("billing_event").unwrap();
let app = init_http_app!(state.clone());
let delivery = json!({
"id": "evt_forged",
"type": "customer.subscription.created",
"data": { "object": { "id": "sub_forged", "status": "active" } },
});
for signature in ["", "t=1,v1=deadbeef"] {
let mut request = test::TestRequest::post()
.uri("/api/billing/webhook")
.header(CONTENT_TYPE, "application/json")
.set_payload(delivery.to_string());
if !signature.is_empty() {
request = request.header("stripe-signature", signature);
}
let resp = test::call_service(&app, request.to_request()).await;
assert_eq!(resp.status().as_u16(), 400, "signature {signature:?}");
}
let rows = state
.db
.raw_json(&format!("SELECT id FROM {events}"), &[])
.await
.unwrap();
assert_eq!(
rows.as_array().map(Vec::len).unwrap_or_default(),
0,
"a refused delivery left a row behind"
);
db.cleanup().await;
fs::remove_dir_all(root).unwrap();
}
#[ntex::test]
async fn the_catalogue_is_public_to_read_and_admin_to_change() {
let db = TempDatabase::create("paycat").await;
let root = temp_dir("paycat");
write_files(&root, &[("main.toml", &paying_app(&db.url))]);
let state = load_state(&root).await;
let (member_token, org) = member_with_role(&state, "member@example.test", "member").await;
let app = init_http_app!(state);
let resp = test::call_service(
&app,
test::TestRequest::get()
.uri("/api/billing_product")
.to_request(),
)
.await;
assert_eq!(resp.status().as_u16(), 200);
let resp = test::call_service(
&app,
bearer(
test::TestRequest::post()
.uri("/api/billing_product")
.header(CONTENT_TYPE, "application/json")
.header("x-organization", org.as_str())
.set_payload(json!({ "name": "Pro" }).to_string()),
&member_token,
)
.to_request(),
)
.await;
assert_eq!(resp.status().as_u16(), 403);
db.cleanup().await;
fs::remove_dir_all(root).unwrap();
}