feat: account deletion request and active-user check
This commit is contained in:
@@ -215,3 +215,41 @@ async fn a_remote_block_publishes_its_event_after_the_k_ap_write() {
|
||||
assert_eq!(blocked, 1);
|
||||
assert_eq!(queued(&pool, "ActorBlocked").await, 1);
|
||||
}
|
||||
|
||||
/// A token outlives the account that owns it, so every authenticated route
|
||||
/// must re-check the account: a deleted user gets exactly what an anonymous
|
||||
/// caller gets.
|
||||
#[tokio::test]
|
||||
async fn a_deleted_users_token_is_rejected() {
|
||||
let (router, pool) = support::server().await;
|
||||
let alice = support::register(&router, "alice", "alice@example.com").await;
|
||||
let bearer = |token: &str| {
|
||||
axum::http::Request::builder()
|
||||
.uri("/api/v1/diary/export?format=json")
|
||||
.header("authorization", format!("Bearer {token}"))
|
||||
.body(axum::body::Body::empty())
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let (status, _, body) = support::send(&router, bearer(&alice.token)).await;
|
||||
assert_eq!(status, 200, "token works while the account is live: {body}");
|
||||
let (status, _, _) =
|
||||
support::send(&router, support::get("/diary/export", Some(&alice.cookie))).await;
|
||||
assert_eq!(status, 200, "cookie works while the account is live");
|
||||
|
||||
sqlx::query("UPDATE users SET deleted_at = '2026-01-01T00:00:00Z' WHERE id = ?")
|
||||
.bind(&alice.user_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (status, _, body) = support::send(&router, bearer(&alice.token)).await;
|
||||
assert_eq!(status, 401, "json route: {body}");
|
||||
let (anon_status, anon_headers, _) =
|
||||
support::send(&router, support::get("/diary/export", None)).await;
|
||||
let (status, headers, _) =
|
||||
support::send(&router, support::get("/diary/export", Some(&alice.cookie))).await;
|
||||
assert_eq!(status, anon_status, "classic owner page");
|
||||
assert_eq!(headers.get("location"), anon_headers.get("location"));
|
||||
assert_eq!(headers.get("location").unwrap(), "/login");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::support::{AcceptingAuth, test_app_state, test_app_state_with_follow_graph};
|
||||
use crate::support::{AcceptingAuth, test_app_state, test_app_state_with_repos};
|
||||
use axum::{
|
||||
Router,
|
||||
body::Body,
|
||||
@@ -6,6 +6,8 @@ use axum::{
|
||||
routing::get,
|
||||
};
|
||||
use domain::errors::DomainError;
|
||||
use domain::ports::UserRepository;
|
||||
use domain::testing::InMemoryStore;
|
||||
use std::sync::Arc;
|
||||
use tower::ServiceExt;
|
||||
use uuid::Uuid;
|
||||
@@ -257,8 +259,19 @@ impl domain::ports::FollowGraphQuery for FixedPendingCount {
|
||||
#[tokio::test]
|
||||
async fn pending_count_endpoint_reports_the_use_case_result() {
|
||||
let uid = domain::value_objects::UserId::from_uuid(Uuid::new_v4());
|
||||
let state = test_app_state_with_follow_graph(
|
||||
Arc::new(FixedPendingCount(3)),
|
||||
let users = InMemoryStore::new();
|
||||
users
|
||||
.save(&domain::testing::test_user_with_role(
|
||||
uid.clone(),
|
||||
domain::models::UserRole::Standard,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let state = test_app_state_with_repos(
|
||||
|repos| {
|
||||
repos.follow_graph = Arc::new(FixedPendingCount(3));
|
||||
repos.user = users as _;
|
||||
},
|
||||
Arc::new(AcceptingAuth(uid)),
|
||||
);
|
||||
let app = Router::new()
|
||||
|
||||
@@ -246,7 +246,8 @@ async fn admin_api_user_rejects_an_unknown_user() {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
// An account the store does not hold is inactive, so it is unauthenticated.
|
||||
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -337,7 +338,9 @@ async fn admin_user_rejects_an_unknown_user() {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||
// An account the store does not hold is inactive: back to the login page.
|
||||
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
|
||||
assert_eq!(resp.headers().get("location").unwrap(), "/login");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -7,7 +7,7 @@ use bootstrap::Repositories;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::GeneratedToken,
|
||||
ports::{AuthService, FollowGraphQuery, PersonQuery, SearchPort, UserRepository},
|
||||
ports::{AuthService, PersonQuery, SearchPort, UserRepository},
|
||||
testing::{
|
||||
PanicAuthService, PanicDiaryExporter, PanicDiaryQuery, PanicDocumentParser,
|
||||
PanicEventPublisher, PanicGoalCommand, PanicGoalQuery, PanicImportProfileRepository,
|
||||
@@ -168,15 +168,15 @@ pub fn test_app_state_with_search_port(
|
||||
build_test_state_from(repos, Arc::new(PanicAuthService))
|
||||
}
|
||||
|
||||
/// Test state with `follow_graph` overridden BEFORE `build_deps` runs, so the
|
||||
/// stub is visible from both `app_ctx.repos.follow_graph` and the prebuilt
|
||||
/// `deps.social.query` that the social handlers actually read from.
|
||||
pub fn test_app_state_with_follow_graph(
|
||||
follow_graph: Arc<dyn FollowGraphQuery>,
|
||||
/// Test state whose repositories are adjusted by `adjust` BEFORE `build_deps`
|
||||
/// runs, for tests that need more than one stub swapped in (e.g. a follow graph
|
||||
/// plus the live user that authenticated routes now look up).
|
||||
pub fn test_app_state_with_repos(
|
||||
adjust: impl FnOnce(&mut Repositories),
|
||||
auth_service: Arc<dyn AuthService>,
|
||||
) -> http_axum::state::AppState {
|
||||
let mut repos = test_repositories();
|
||||
repos.follow_graph = follow_graph;
|
||||
adjust(&mut repos);
|
||||
build_test_state_from(repos, auth_service)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user