Files
movies-diary/crates/bootstrap/tests/http_handlers/extractors_test.rs
Gabriel Kaszewski a1c3f7760d test: move http-axum tests to tests/
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).
2026-10-25 22:52:41 +01:00

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);
}