Drop bootstrap dev-dep. Extractor + api handler + app-context tests (need full AppState via build_deps) -> bootstrap/tests/http_handlers/. forms tests -> http-axum/tests/log_review_form_test.rs. Federation collection tests now go through public federation::html_routes(). Kept inline (private fns): api/social social_actor_to_dto (4), federation/blocklist_api social_actor_to_blocked_dto (1), html/social social_actor_to_template (1), router slow_down/api_governor (2).
362 lines
10 KiB
Rust
362 lines
10 KiB
Rust
use std::sync::Arc;
|
|
|
|
use axum::{
|
|
Router,
|
|
body::Body,
|
|
http::{Request, StatusCode},
|
|
routing::get,
|
|
};
|
|
use domain::{
|
|
errors::DomainError, models::GeneratedToken, ports::AuthService, testing::InMemoryStore,
|
|
value_objects::UserId,
|
|
};
|
|
use http_axum::extractors::{
|
|
AdminApiUser, AdminUser, AuthenticatedUser, OptionalCookieUser, RequiredCookieUser,
|
|
};
|
|
use tower::ServiceExt;
|
|
|
|
use crate::support::{
|
|
AcceptingAuth, ErroringUserRepo, make_test_state, test_app_state, test_app_state_with_user,
|
|
};
|
|
|
|
struct RejectingAuth;
|
|
#[async_trait::async_trait]
|
|
impl AuthService for RejectingAuth {
|
|
async fn generate_token(&self, _: &UserId) -> Result<GeneratedToken, DomainError> {
|
|
panic!()
|
|
}
|
|
async fn validate_token(&self, _: &str) -> Result<UserId, DomainError> {
|
|
Err(DomainError::Unauthorized("bad token".into()))
|
|
}
|
|
}
|
|
|
|
// --- Routers ---
|
|
|
|
async fn protected_handler(user: AuthenticatedUser) -> String {
|
|
user.0.value().to_string()
|
|
}
|
|
async fn optional_cookie_handler(user: OptionalCookieUser) -> String {
|
|
match user.0 {
|
|
Some(id) => id.value().to_string(),
|
|
None => "none".to_string(),
|
|
}
|
|
}
|
|
async fn required_cookie_handler(user: RequiredCookieUser) -> String {
|
|
user.0.value().to_string()
|
|
}
|
|
async fn admin_api_handler(user: AdminApiUser) -> String {
|
|
user.0.value().to_string()
|
|
}
|
|
async fn admin_cookie_handler(user: AdminUser) -> String {
|
|
user.0.value().to_string()
|
|
}
|
|
|
|
fn router_protected(state: http_axum::state::AppState) -> Router {
|
|
Router::new()
|
|
.route("/protected", get(protected_handler))
|
|
.with_state(state)
|
|
}
|
|
fn router_optional(state: http_axum::state::AppState) -> Router {
|
|
Router::new()
|
|
.route("/optional", get(optional_cookie_handler))
|
|
.with_state(state)
|
|
}
|
|
fn router_admin_api(state: http_axum::state::AppState) -> Router {
|
|
Router::new()
|
|
.route("/admin-api", get(admin_api_handler))
|
|
.with_state(state)
|
|
}
|
|
fn router_admin_cookie(state: http_axum::state::AppState) -> Router {
|
|
Router::new()
|
|
.route("/admin-cookie", get(admin_cookie_handler))
|
|
.with_state(state)
|
|
}
|
|
fn router_required(state: http_axum::state::AppState) -> Router {
|
|
Router::new()
|
|
.route("/required", get(required_cookie_handler))
|
|
.with_state(state)
|
|
}
|
|
|
|
// --- Tests ---
|
|
|
|
#[tokio::test]
|
|
async fn missing_auth_header_returns_401() {
|
|
let app = router_protected(test_app_state());
|
|
let resp = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/protected")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn optional_cookie_user_returns_none_without_cookie() {
|
|
let app = router_optional(test_app_state());
|
|
let resp = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/optional")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(&body[..], b"none");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn optional_cookie_user_returns_none_with_invalid_token() {
|
|
let app = router_optional(make_test_state(Arc::new(RejectingAuth)));
|
|
let resp = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/optional")
|
|
.header("cookie", "token=bad.token.here")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(&body[..], b"none");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn required_cookie_user_redirects_without_cookie() {
|
|
let app = router_required(test_app_state());
|
|
let resp = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/required")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
|
|
assert_eq!(resp.headers().get("location").unwrap(), "/login");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn required_cookie_user_redirects_with_invalid_token() {
|
|
let app = router_required(make_test_state(Arc::new(RejectingAuth)));
|
|
let resp = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/required")
|
|
.header("cookie", "token=bad.token.here")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
|
|
assert_eq!(resp.headers().get("location").unwrap(), "/login");
|
|
}
|
|
|
|
// --- AdminApiUser / AdminUser: per-path rejection pinning ---
|
|
//
|
|
// These pin the exact rejection for every path `authorize_admin::execute` can
|
|
// produce, now that both extractors go through it instead of calling
|
|
// `repos.user.find_by_id` directly. Each test name states the path; the
|
|
// asserted status is the byte-identical-to-before-this-task value recorded in
|
|
// the task-4 report's before/after table.
|
|
|
|
fn admin_id() -> UserId {
|
|
UserId::generate()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn admin_api_user_allows_an_admin_user() {
|
|
let id = admin_id();
|
|
let user_repo = InMemoryStore::new();
|
|
user_repo.seed_user(domain::testing::test_user_with_role(
|
|
id.clone(),
|
|
domain::models::UserRole::Admin,
|
|
));
|
|
let app = router_admin_api(test_app_state_with_user(
|
|
user_repo as _,
|
|
Arc::new(AcceptingAuth(id)),
|
|
));
|
|
let resp = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/admin-api")
|
|
.header("authorization", "Bearer anything")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn admin_api_user_rejects_a_non_admin_user() {
|
|
let id = admin_id();
|
|
let user_repo = InMemoryStore::new();
|
|
user_repo.seed_user(domain::testing::test_user_with_role(
|
|
id.clone(),
|
|
domain::models::UserRole::Standard,
|
|
));
|
|
let app = router_admin_api(test_app_state_with_user(
|
|
user_repo as _,
|
|
Arc::new(AcceptingAuth(id)),
|
|
));
|
|
let resp = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/admin-api")
|
|
.header("authorization", "Bearer anything")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn admin_api_user_rejects_an_unknown_user() {
|
|
let id = admin_id();
|
|
let user_repo = InMemoryStore::new(); // empty — id is not in the store
|
|
let app = router_admin_api(test_app_state_with_user(
|
|
user_repo as _,
|
|
Arc::new(AcceptingAuth(id)),
|
|
));
|
|
let resp = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/admin-api")
|
|
.header("authorization", "Bearer anything")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn admin_api_user_surfaces_a_repository_error_as_500() {
|
|
let id = admin_id();
|
|
let app = router_admin_api(test_app_state_with_user(
|
|
Arc::new(ErroringUserRepo),
|
|
Arc::new(AcceptingAuth(id)),
|
|
));
|
|
let resp = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/admin-api")
|
|
.header("authorization", "Bearer anything")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn admin_user_allows_an_admin_user() {
|
|
let id = admin_id();
|
|
let user_repo = InMemoryStore::new();
|
|
user_repo.seed_user(domain::testing::test_user_with_role(
|
|
id.clone(),
|
|
domain::models::UserRole::Admin,
|
|
));
|
|
let app = router_admin_cookie(test_app_state_with_user(
|
|
user_repo as _,
|
|
Arc::new(AcceptingAuth(id)),
|
|
));
|
|
let resp = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/admin-cookie")
|
|
.header("cookie", "token=anything")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn admin_user_rejects_a_non_admin_user() {
|
|
let id = admin_id();
|
|
let user_repo = InMemoryStore::new();
|
|
user_repo.seed_user(domain::testing::test_user_with_role(
|
|
id.clone(),
|
|
domain::models::UserRole::Standard,
|
|
));
|
|
let app = router_admin_cookie(test_app_state_with_user(
|
|
user_repo as _,
|
|
Arc::new(AcceptingAuth(id)),
|
|
));
|
|
let resp = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/admin-cookie")
|
|
.header("cookie", "token=anything")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn admin_user_rejects_an_unknown_user() {
|
|
let id = admin_id();
|
|
let user_repo = InMemoryStore::new(); // empty — id is not in the store
|
|
let app = router_admin_cookie(test_app_state_with_user(
|
|
user_repo as _,
|
|
Arc::new(AcceptingAuth(id)),
|
|
));
|
|
let resp = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/admin-cookie")
|
|
.header("cookie", "token=anything")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn admin_user_surfaces_a_repository_error_as_500() {
|
|
let id = admin_id();
|
|
let app = router_admin_cookie(test_app_state_with_user(
|
|
Arc::new(ErroringUserRepo),
|
|
Arc::new(AcceptingAuth(id)),
|
|
));
|
|
let resp = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.uri("/admin-cookie")
|
|
.header("cookie", "token=anything")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
|
}
|