use axum::{
body::Body,
http::{header, Request, StatusCode},
routing::get,
Router,
};
use base64::{engine::general_purpose::STANDARD, Engine};
use rok_auth::Claims;
use rok_auth_basic::BasicAuthLayer;
use tower::ServiceExt;
fn test_app() -> Router {
Router::new()
.route("/", get(|| async { "hello" }))
.layer(BasicAuthLayer::new(
"Test Realm",
|user: String, pass: String| async move {
if user == "admin" && pass == "secret" {
Some(Claims::new(user, vec!["admin"]))
} else {
None
}
},
))
}
fn basic_header(user: &str, pass: &str) -> String {
format!("Basic {}", STANDARD.encode(format!("{user}:{pass}")))
}
#[tokio::test]
async fn valid_credentials_returns_200() {
let app = test_app();
let response = app
.oneshot(
Request::builder()
.uri("/")
.header(header::AUTHORIZATION, basic_header("admin", "secret"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn wrong_password_returns_401() {
let app = test_app();
let response = app
.oneshot(
Request::builder()
.uri("/")
.header(header::AUTHORIZATION, basic_header("admin", "wrong"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn wrong_username_returns_401() {
let app = test_app();
let response = app
.oneshot(
Request::builder()
.uri("/")
.header(header::AUTHORIZATION, basic_header("notadmin", "secret"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn missing_header_returns_401() {
let app = test_app();
let response = app
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn missing_header_includes_www_authenticate_challenge() {
let app = test_app();
let response = app
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
let www_auth = response
.headers()
.get(header::WWW_AUTHENTICATE)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
assert!(
www_auth.starts_with("Basic realm="),
"expected WWW-Authenticate: Basic realm=..., got: {www_auth}"
);
assert!(www_auth.contains("Test Realm"));
}
#[tokio::test]
async fn bearer_token_header_is_rejected() {
let app = test_app();
let response = app
.oneshot(
Request::builder()
.uri("/")
.header(header::AUTHORIZATION, "Bearer some.jwt.token")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn malformed_base64_returns_401() {
let app = test_app();
let response = app
.oneshot(
Request::builder()
.uri("/")
.header(header::AUTHORIZATION, "Basic !!!not-valid-base64!!!")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn parse_basic_header_valid() {
use axum::http::HeaderMap;
use rok_auth_basic::parse_basic_header;
let mut headers = HeaderMap::new();
headers.insert(
header::AUTHORIZATION,
basic_header("alice", "p@ss:word").parse().unwrap(),
);
let result = parse_basic_header(&headers);
assert_eq!(result, Some(("alice".to_string(), "p@ss:word".to_string())));
}
#[test]
fn parse_basic_header_missing() {
use axum::http::HeaderMap;
use rok_auth_basic::parse_basic_header;
let headers = HeaderMap::new();
assert_eq!(parse_basic_header(&headers), None);
}
#[test]
fn parse_basic_header_password_with_colon() {
use axum::http::HeaderMap;
use rok_auth_basic::parse_basic_header;
let mut headers = HeaderMap::new();
headers.insert(
header::AUTHORIZATION,
basic_header("user", "pass:with:colons").parse().unwrap(),
);
let (u, p) = parse_basic_header(&headers).unwrap();
assert_eq!(u, "user");
assert_eq!(p, "pass:with:colons");
}