use axum::Router;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::response::Response;
use serde_json::{Value, json};
use tower::ServiceExt;
mod common;
use common::{EcSigner, TestSigner, body_json, fetch_nonce, p, test_app};
const NEW_ACCOUNT_URL: &str = "http://localhost:3000/profile/default/newAccount";
async fn post(app: &Router, path: &str, body: String) -> Response {
app.clone()
.oneshot(
Request::post(path)
.header("content-type", "application/jose+json")
.body(Body::from(body))
.unwrap(),
)
.await
.unwrap()
}
async fn register_with(app: &Router, signer: &impl TestSigner, contact: Value) -> Response {
let nonce = fetch_nonce(app).await;
post(
app,
&p("/newAccount"),
signer.sign(
NEW_ACCOUNT_URL,
&nonce,
&json!({ "termsOfServiceAgreed": true, "contact": contact }),
),
)
.await
}
#[tokio::test]
async fn a_non_mailto_scheme_is_unsupported_contact() {
let app = test_app().await;
for contact in [
"tel:+15551234567",
"https://example.com/contact",
"not-a-url-at-all",
] {
let signer = EcSigner::new();
let res = register_with(&app, &signer, json!([contact])).await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST, "{contact}");
assert_eq!(
body_json(res).await["type"],
"urn:ietf:params:acme:error:unsupportedContact",
"{contact}"
);
}
}
#[tokio::test]
async fn a_malformed_mailto_is_invalid_contact() {
let app = test_app().await;
for (name, contact) in [
("hfields", "mailto:admin@example.com?subject=hi"),
("two addresses", "mailto:a@example.com,b@example.com"),
("no address", "mailto:"),
("no domain", "mailto:admin"),
("no local part", "mailto:@example.com"),
("a domain with no dot", "mailto:admin@localhost"),
] {
let signer = EcSigner::new();
let res = register_with(&app, &signer, json!([contact])).await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST, "{name}");
assert_eq!(
body_json(res).await["type"],
"urn:ietf:params:acme:error:invalidContact",
"{name}"
);
}
}
#[tokio::test]
async fn ordinary_addresses_are_accepted_and_stored() {
let app = test_app().await;
for contact in [
"mailto:admin@example.com",
"mailto:cert-admin+acme@sub.example.co.uk",
"mailto:weird.but-legal_name@example.org",
] {
let signer = EcSigner::new();
let res = register_with(&app, &signer, json!([contact])).await;
assert_eq!(res.status(), StatusCode::CREATED, "{contact}");
assert_eq!(body_json(res).await["contact"], json!([contact]));
}
let signer = EcSigner::new();
let res = register_with(&app, &signer, json!([])).await;
assert_eq!(res.status(), StatusCode::CREATED);
assert!(body_json(res).await.get("contact").is_none());
}
#[tokio::test]
async fn one_bad_address_rejects_the_whole_list() {
let app = test_app().await;
let signer = EcSigner::new();
let res = register_with(
&app,
&signer,
json!(["mailto:good@example.com", "tel:+15551234567"]),
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
assert_eq!(
body_json(res).await["type"],
"urn:ietf:params:acme:error:unsupportedContact"
);
}
#[tokio::test]
async fn a_contact_update_is_validated_as_well() {
let app = test_app().await;
let signer = EcSigner::new();
let res = register_with(&app, &signer, json!(["mailto:admin@example.com"])).await;
assert_eq!(res.status(), StatusCode::CREATED);
let account_url = res
.headers()
.get("location")
.and_then(|v| v.to_str().ok())
.unwrap()
.to_string();
let path = account_url.strip_prefix(common::HOST).unwrap();
let nonce = fetch_nonce(&app).await;
let res = post(
&app,
path,
signer.sign_kid(
&account_url,
&account_url,
&nonce,
&json!({ "contact": ["tel:+15551234567"] }),
),
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
assert_eq!(
body_json(res).await["type"],
"urn:ietf:params:acme:error:unsupportedContact"
);
let nonce = fetch_nonce(&app).await;
let res = post(
&app,
path,
signer.sign_kid(
&account_url,
&account_url,
&nonce,
&json!({ "contact": ["mailto:new@example.com"] }),
),
)
.await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
body_json(res).await["contact"],
json!(["mailto:new@example.com"])
);
}
#[tokio::test]
async fn an_account_carrying_too_many_contacts_is_refused() {
let app = test_app().await;
let signer = EcSigner::new();
let contacts: Vec<String> = (0..33)
.map(|n| format!("mailto:a{n}@example.com"))
.collect();
let nonce = fetch_nonce(&app).await;
let body = signer.sign(
NEW_ACCOUNT_URL,
&nonce,
&json!({ "termsOfServiceAgreed": true, "contact": contacts }),
);
let res = post(&app, &p("/newAccount"), body).await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let problem = body_json(res).await;
assert_eq!(problem["type"], "urn:ietf:params:acme:error:invalidContact");
let nonce = fetch_nonce(&app).await;
let body = signer.sign(
NEW_ACCOUNT_URL,
&nonce,
&json!({
"termsOfServiceAgreed": true,
"contact": ["mailto:ops@example.com", "mailto:security@example.com"],
}),
);
assert_eq!(
post(&app, &p("/newAccount"), body).await.status(),
StatusCode::CREATED
);
}