use async_trait::async_trait;
use axum::body::{to_bytes, Body};
use axum::http::{Request, StatusCode};
use rusmes_auth::AuthBackend;
use rusmes_jmap::{Account, JmapServer, Session, SharedAuth};
use rusmes_proto::Username;
use std::sync::Arc;
use tower::ServiceExt;
struct AliceBackend;
#[async_trait]
impl AuthBackend for AliceBackend {
async fn authenticate(&self, username: &Username, password: &str) -> anyhow::Result<bool> {
Ok(username.as_str() == "alice" && password == "hunter2")
}
async fn verify_identity(&self, username: &Username) -> anyhow::Result<bool> {
Ok(username.as_str() == "alice")
}
async fn list_users(&self) -> anyhow::Result<Vec<Username>> {
Ok(vec![Username::new("alice".to_string())?])
}
async fn create_user(&self, _u: &Username, _p: &str) -> anyhow::Result<()> {
anyhow::bail!("read-only test backend")
}
async fn delete_user(&self, _u: &Username) -> anyhow::Result<()> {
anyhow::bail!("read-only test backend")
}
async fn change_password(&self, _u: &Username, _p: &str) -> anyhow::Result<()> {
anyhow::bail!("read-only test backend")
}
}
fn router_with_alice() -> axum::Router {
let auth: SharedAuth = Arc::new(AliceBackend);
JmapServer::routes_with_auth(auth)
}
const ALICE_BASIC: &str = "Basic YWxpY2U6aHVudGVyMg==";
const ALICE_BASIC_BAD: &str = "Basic YWxpY2U6d3Jvbmc=";
#[tokio::test]
async fn no_authorization_header_returns_401() {
let app = router_with_alice();
let req = Request::builder()
.method("GET")
.uri("/.well-known/jmap")
.body(Body::empty())
.expect("build request");
let resp = app.oneshot(req).await.expect("dispatch");
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let www = resp
.headers()
.get(axum::http::header::WWW_AUTHENTICATE)
.expect("WWW-Authenticate header present");
assert!(www.to_str().unwrap_or_default().starts_with("Basic"));
}
#[tokio::test]
async fn malformed_authorization_header_returns_401() {
let app = router_with_alice();
let req = Request::builder()
.method("GET")
.uri("/.well-known/jmap")
.header(axum::http::header::AUTHORIZATION, "totally not valid")
.body(Body::empty())
.expect("build request");
let resp = app.oneshot(req).await.expect("dispatch");
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn bearer_token_currently_rejected() {
let app = router_with_alice();
let req = Request::builder()
.method("GET")
.uri("/.well-known/jmap")
.header(
axum::http::header::AUTHORIZATION,
"Bearer some-jwt-looking.token.value",
)
.body(Body::empty())
.expect("build request");
let resp = app.oneshot(req).await.expect("dispatch");
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn basic_auth_wrong_password_returns_401() {
let app = router_with_alice();
let req = Request::builder()
.method("GET")
.uri("/.well-known/jmap")
.header(axum::http::header::AUTHORIZATION, ALICE_BASIC_BAD)
.body(Body::empty())
.expect("build request");
let resp = app.oneshot(req).await.expect("dispatch");
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn basic_auth_unknown_user_returns_401() {
let app = router_with_alice();
let req = Request::builder()
.method("GET")
.uri("/.well-known/jmap")
.header(axum::http::header::AUTHORIZATION, "Basic Ym9iOmh1bnRlcjI=")
.body(Body::empty())
.expect("build request");
let resp = app.oneshot(req).await.expect("dispatch");
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn basic_auth_alice_returns_session_with_alice_principal() {
let app = router_with_alice();
let req = Request::builder()
.method("GET")
.uri("/.well-known/jmap")
.header(axum::http::header::AUTHORIZATION, ALICE_BASIC)
.body(Body::empty())
.expect("build request");
let resp = app.oneshot(req).await.expect("dispatch");
assert_eq!(resp.status(), StatusCode::OK);
let body = to_bytes(resp.into_body(), 1 << 20)
.await
.expect("read body");
let session: Session = serde_json::from_slice(&body).expect("parse session");
assert_eq!(session.username, "alice");
let account_id = session
.accounts
.keys()
.next()
.expect("at least one account")
.clone();
assert_eq!(account_id, "account-alice");
let acct: &Account = session
.accounts
.get(&account_id)
.expect("account is present");
assert_eq!(acct.name, "alice");
assert!(acct.is_personal);
}
#[tokio::test]
async fn auth_less_routes_reject_everything_with_401() {
let app = JmapServer::routes();
let req = Request::builder()
.method("GET")
.uri("/.well-known/jmap")
.header(axum::http::header::AUTHORIZATION, ALICE_BASIC)
.body(Body::empty())
.expect("build request");
let resp = app.oneshot(req).await.expect("dispatch");
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}