mod common;
use std::net::SocketAddr;
use arium::{AuditConfig, AuthConfig, Mailer, RECOMMENDED_HSTS};
use axum::Router;
use axum::routing::get;
use reqwest::Client;
use tokio::net::TcpListener;
async fn boot(
customize: impl FnOnce(arium::AuthConfigBuilder) -> arium::AuthConfigBuilder,
) -> String {
let pool = common::pool().await;
let mailer = Mailer::from_env().expect("mailer build");
let builder = AuthConfig::builder(pool, mailer)
.rate_limit(None)
.audit(AuditConfig {
capture_ip: false,
capture_user_agent: false,
retention_days: 0,
});
let cfg = customize(builder).build().unwrap();
let router: Router =
arium::install(Router::new().route("/__ping", get(|| async { "ok" })), cfg)
.await
.expect("install");
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr: SocketAddr = listener.local_addr().expect("local_addr");
tokio::spawn(async move {
let _ = axum::serve(
listener,
router.into_make_service_with_connect_info::<SocketAddr>(),
)
.await;
});
format!("http://{addr}")
}
fn session_set_cookie(resp: &reqwest::Response) -> String {
resp.headers()
.get_all("set-cookie")
.iter()
.map(|v| v.to_str().unwrap().to_string())
.find(|c| c.starts_with("session="))
.expect("a session= Set-Cookie header")
}
#[tokio::test]
async fn session_cookie_is_lax_httponly_and_insecure_by_default() {
let base = boot(|b| b).await;
let resp = Client::new()
.get(format!("{base}/__ping"))
.send()
.await
.expect("request");
let cookie = session_set_cookie(&resp);
assert!(cookie.contains("SameSite=Lax"), "{cookie}");
assert!(cookie.contains("HttpOnly"), "{cookie}");
assert!(
!cookie.to_lowercase().contains("secure"),
"session cookie should not be Secure by default: {cookie}"
);
}
#[tokio::test]
async fn session_cookie_secure_when_opted_in() {
let base = boot(|b| b.cookie_secure(true)).await;
let resp = Client::new()
.get(format!("{base}/__ping"))
.send()
.await
.expect("request");
let cookie = session_set_cookie(&resp);
assert!(cookie.contains("Secure"), "{cookie}");
assert!(cookie.contains("SameSite=Lax"), "{cookie}");
assert!(cookie.contains("HttpOnly"), "{cookie}");
}
#[tokio::test]
async fn static_security_headers_present_by_default() {
let base = boot(|b| b).await;
let resp = Client::new()
.get(format!("{base}/__ping"))
.send()
.await
.expect("request");
let h = resp.headers();
assert_eq!(h.get("x-content-type-options").unwrap(), "nosniff");
assert_eq!(
h.get("referrer-policy").unwrap(),
"strict-origin-when-cross-origin"
);
assert_eq!(h.get("x-frame-options").unwrap(), "SAMEORIGIN");
assert_eq!(h.get("cross-origin-opener-policy").unwrap(), "same-origin");
assert_eq!(h.get("x-permitted-cross-domain-policies").unwrap(), "none");
assert_eq!(
h.get("permissions-policy").unwrap(),
"camera=(), microphone=(), geolocation=()"
);
assert!(h.get("strict-transport-security").is_none());
assert!(h.get("content-security-policy").is_none());
}
#[tokio::test]
async fn hsts_and_csp_emitted_when_opted_in() {
let csp = "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'";
let base = boot(|b| b.hsts(RECOMMENDED_HSTS).content_security_policy(csp)).await;
let resp = Client::new()
.get(format!("{base}/__ping"))
.send()
.await
.expect("request");
let h = resp.headers();
assert_eq!(
h.get("strict-transport-security").unwrap(),
RECOMMENDED_HSTS
);
assert_eq!(h.get("content-security-policy").unwrap(), csp);
assert_eq!(h.get("x-content-type-options").unwrap(), "nosniff");
}