feat: account deletion request and active-user check
This commit is contained in:
@@ -21,7 +21,9 @@ pub async fn execute(deps: &RegisterDeps, cmd: RegisterCommand) -> Result<(), Do
|
||||
));
|
||||
}
|
||||
|
||||
if deps.user.find_by_username(&username).await?.is_some() {
|
||||
if deps.user.find_by_username(&username).await?.is_some()
|
||||
|| deps.user.is_username_retired(&username).await?
|
||||
{
|
||||
return Err(DomainError::ValidationError(
|
||||
"Username already taken".into(),
|
||||
));
|
||||
|
||||
@@ -26,10 +26,10 @@ use crate::person::deps::{EnrichPersonDeps, GetPersonDeps};
|
||||
use crate::search::deps::SearchDeps;
|
||||
use crate::social::deps::{SocialCommandDeps, SocialQueryDeps};
|
||||
use crate::users::deps::{
|
||||
AuthorizeAdminDeps, DeleteAccountDeps, GetCurrentProfileDeps, GetFederatedProfileDeps,
|
||||
AuthorizeAdminDeps, EnsureActiveDeps, GetCurrentProfileDeps, GetFederatedProfileDeps,
|
||||
GetFederatedProfileStatsDeps, GetLocalProfileDeps, GetPageViewerDeps, GetProfileSettingsDeps,
|
||||
GetSettingsDeps, GetUsersListDeps, ResolveUsernameDeps, UpdateProfileDeps,
|
||||
UpdateProfileFieldsDeps, UpdateSettingsDeps,
|
||||
GetSettingsDeps, GetUsersListDeps, RequestAccountDeletionDeps, ResolveUsernameDeps,
|
||||
UpdateProfileDeps, UpdateProfileFieldsDeps, UpdateSettingsDeps,
|
||||
};
|
||||
use crate::watchlist::deps::{
|
||||
GetWatchlistDeps, GetWatchlistForOwnerDeps, IsOnWatchlistDeps, RemoveFromWatchlistDeps,
|
||||
@@ -124,8 +124,8 @@ pub struct UsersGroup {
|
||||
pub get_profile_settings: GetProfileSettingsDeps,
|
||||
pub get_users_list: GetUsersListDeps,
|
||||
pub update_profile: UpdateProfileDeps,
|
||||
/// Not reachable from the server binary; see the `Deps`-level note above. Pre-existing dead use case: `users::delete_account::execute` has zero callers anywhere in the workspace, worker included.
|
||||
pub delete_account: DeleteAccountDeps,
|
||||
pub request_account_deletion: RequestAccountDeletionDeps,
|
||||
pub ensure_active: EnsureActiveDeps,
|
||||
pub get_current_profile: GetCurrentProfileDeps,
|
||||
pub update_profile_fields: UpdateProfileFieldsDeps,
|
||||
pub get_settings: GetSettingsDeps,
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
use domain::{errors::DomainError, events::DomainEvent, value_objects::UserId};
|
||||
|
||||
use crate::users::deps::DeleteAccountDeps;
|
||||
|
||||
pub async fn execute(deps: &DeleteAccountDeps, user_id: uuid::Uuid) -> Result<(), DomainError> {
|
||||
let uid = UserId::from_uuid(user_id);
|
||||
|
||||
deps.user
|
||||
.find_by_id(&uid)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("User not found".into()))?;
|
||||
|
||||
// Notify federation peers before any data is removed so they can process the tombstone.
|
||||
deps.event_publisher
|
||||
.publish(&DomainEvent::UserDeleted { user_id: uid })
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use domain::ports::{
|
||||
DiaryQuery, EventPublisher, FederatedProfileQuery, FederationAdminQuery, FollowGraphQuery,
|
||||
ObjectStorage, StatsRepository, UserProfileFieldsRepository, UserRepository,
|
||||
ObjectStorage, PasswordHasher, StatsRepository, UserProfileFieldsRepository, UserRepository,
|
||||
UserSettingsRepository,
|
||||
};
|
||||
use domain::value_objects::InstanceIdentity;
|
||||
@@ -47,9 +47,13 @@ pub struct UpdateProfileDeps {
|
||||
pub event_publisher: Arc<dyn EventPublisher>,
|
||||
}
|
||||
|
||||
pub struct DeleteAccountDeps {
|
||||
pub struct RequestAccountDeletionDeps {
|
||||
pub user: Arc<dyn UserRepository>,
|
||||
pub password_hasher: Arc<dyn PasswordHasher>,
|
||||
}
|
||||
|
||||
pub struct EnsureActiveDeps {
|
||||
pub user: Arc<dyn UserRepository>,
|
||||
pub event_publisher: Arc<dyn EventPublisher>,
|
||||
}
|
||||
|
||||
pub struct GetCurrentProfileDeps {
|
||||
|
||||
12
crates/application/src/users/ensure_active.rs
Normal file
12
crates/application/src/users/ensure_active.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use domain::{errors::DomainError, value_objects::UserId};
|
||||
|
||||
use crate::users::deps::EnsureActiveDeps;
|
||||
|
||||
/// A token outlives its account; this is the check that stops a deleted
|
||||
/// (or vanished) user from acting on one.
|
||||
pub async fn execute(deps: &EnsureActiveDeps, user: &UserId) -> Result<(), DomainError> {
|
||||
match deps.user.find_by_id(user).await? {
|
||||
Some(_) => Ok(()),
|
||||
None => Err(DomainError::Unauthorized("account is not active".into())),
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
pub mod authorize_admin;
|
||||
pub mod commands;
|
||||
pub mod delete_account;
|
||||
pub mod deps;
|
||||
mod diary_filter;
|
||||
pub mod ensure_active;
|
||||
pub mod get_current_profile;
|
||||
pub mod get_federated_profile;
|
||||
pub mod get_federated_profile_stats;
|
||||
@@ -12,6 +12,7 @@ pub mod get_profile_settings;
|
||||
pub mod get_settings;
|
||||
pub mod get_users;
|
||||
pub mod queries;
|
||||
pub mod request_account_deletion;
|
||||
pub mod resolve_username_to_id;
|
||||
pub mod update_profile;
|
||||
pub mod update_profile_fields;
|
||||
|
||||
60
crates/application/src/users/request_account_deletion.rs
Normal file
60
crates/application/src/users/request_account_deletion.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use domain::{errors::DomainError, models::UserRole, value_objects::UserId};
|
||||
|
||||
use crate::users::deps::RequestAccountDeletionDeps;
|
||||
|
||||
/// Tombstones `target`. A user may delete themselves; an admin may delete
|
||||
/// anyone. Either way the *actor's* password is the proof, and the last
|
||||
/// active admin can never be removed. `UserDeleted` rides the repository
|
||||
/// write, so the tombstone and its event commit together.
|
||||
pub async fn execute(
|
||||
deps: &RequestAccountDeletionDeps,
|
||||
actor: UserId,
|
||||
target: UserId,
|
||||
password: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
let actor_user = deps
|
||||
.user
|
||||
.find_by_id(&actor)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::Unauthorized("account is not active".into()))?;
|
||||
|
||||
let is_self = actor == target;
|
||||
if !is_self && !matches!(actor_user.role(), UserRole::Admin) {
|
||||
return Err(DomainError::Forbidden(
|
||||
"only an admin can delete another account".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if !deps
|
||||
.password_hasher
|
||||
.verify(password, actor_user.password_hash())
|
||||
.await?
|
||||
{
|
||||
return Err(DomainError::Unauthorized("incorrect password".into()));
|
||||
}
|
||||
|
||||
let target_user = if is_self {
|
||||
actor_user
|
||||
} else {
|
||||
deps.user
|
||||
.find_by_id(&target)
|
||||
.await?
|
||||
.ok_or_else(|| DomainError::NotFound("User not found".into()))?
|
||||
};
|
||||
|
||||
if matches!(target_user.role(), UserRole::Admin) && deps.user.count_active_admins().await? <= 1
|
||||
{
|
||||
return Err(DomainError::Conflict(
|
||||
"the last active admin cannot be deleted".into(),
|
||||
));
|
||||
}
|
||||
|
||||
deps.user
|
||||
.mark_deleted(
|
||||
&target,
|
||||
&[domain::events::DomainEvent::UserDeleted {
|
||||
user_id: target.clone(),
|
||||
}],
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -71,3 +71,34 @@ async fn registering_with_a_short_password_fails() {
|
||||
"expected password length error, got: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn registration_refuses_a_retired_username() {
|
||||
let users = InMemoryStore::new();
|
||||
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||
let deps = RegisterDeps {
|
||||
user: b.user_repo.clone(),
|
||||
password_hasher: b.password_hasher.clone(),
|
||||
config: b.config.clone(),
|
||||
};
|
||||
register::execute(&deps, register_command("old@example.com", "ghost"))
|
||||
.await
|
||||
.unwrap();
|
||||
let old = users
|
||||
.find_by_email(&Email::new("old@example.com".into()).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
users.mark_deleted(old.id(), &[]).await.unwrap();
|
||||
// The purge removes the row; only the retired name keeps the username taken.
|
||||
users.delete_user(old.id());
|
||||
|
||||
let err = register::execute(&deps, register_command("new@example.com", "Ghost"))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(
|
||||
matches!(&err, domain::errors::DomainError::ValidationError(m) if m == "Username already taken"),
|
||||
"got {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ mod support;
|
||||
|
||||
#[path = "users/authorize_admin_test.rs"]
|
||||
mod authorize_admin_test;
|
||||
#[path = "users/delete_account_test.rs"]
|
||||
mod delete_account_test;
|
||||
#[path = "users/ensure_active_test.rs"]
|
||||
mod ensure_active_test;
|
||||
#[path = "users/get_current_profile_test.rs"]
|
||||
mod get_current_profile_test;
|
||||
#[path = "users/get_federated_profile_stats_test.rs"]
|
||||
@@ -20,6 +20,8 @@ mod get_profile_settings_test;
|
||||
mod get_settings_test;
|
||||
#[path = "users/get_users_test.rs"]
|
||||
mod get_users_test;
|
||||
#[path = "users/request_account_deletion_test.rs"]
|
||||
mod request_account_deletion_test;
|
||||
#[path = "users/resolve_username_to_id_test.rs"]
|
||||
mod resolve_username_to_id_test;
|
||||
#[path = "users/update_profile_fields_test.rs"]
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
use crate::support::{TestContextBuilder, register_command};
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
ports::UserRepository,
|
||||
testing::{InMemoryStore, NoopEventPublisher, RefusingEventPublisher},
|
||||
value_objects::{Email, UserId},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::{
|
||||
auth::{deps::RegisterDeps, register},
|
||||
users::{delete_account, deps::DeleteAccountDeps},
|
||||
};
|
||||
|
||||
async fn register_user(b: &TestContextBuilder, users: &Arc<InMemoryStore>) -> Uuid {
|
||||
let reg_deps = RegisterDeps {
|
||||
user: b.user_repo.clone(),
|
||||
password_hasher: b.password_hasher.clone(),
|
||||
config: b.config.clone(),
|
||||
};
|
||||
register::execute(®_deps, register_command("alice@example.com", "alice"))
|
||||
.await
|
||||
.unwrap();
|
||||
users
|
||||
.find_by_email(&Email::new("alice@example.com".into()).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.id()
|
||||
.value()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_an_unknown_user_is_not_found() {
|
||||
let events = NoopEventPublisher::new();
|
||||
let b = TestContextBuilder::new();
|
||||
let deps = DeleteAccountDeps {
|
||||
user: b.user_repo.clone(),
|
||||
event_publisher: Arc::clone(&events) as _,
|
||||
};
|
||||
|
||||
let err = delete_account::execute(&deps, Uuid::new_v4())
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, DomainError::NotFound(_)));
|
||||
assert!(events.published().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_an_existing_user_publishes_user_deleted() {
|
||||
let users = InMemoryStore::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||
let deps = DeleteAccountDeps {
|
||||
user: b.user_repo.clone(),
|
||||
event_publisher: Arc::clone(&events) as _,
|
||||
};
|
||||
let uid = register_user(&b, &users).await;
|
||||
|
||||
delete_account::execute(&deps, uid).await.unwrap();
|
||||
|
||||
let published = events.published();
|
||||
assert!(
|
||||
matches!(
|
||||
published.as_slice(),
|
||||
[DomainEvent::UserDeleted { user_id }] if *user_id == UserId::from_uuid(uid)
|
||||
),
|
||||
"published: {published:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deleting_fails_when_the_publisher_refuses() {
|
||||
let users = InMemoryStore::new();
|
||||
let b = TestContextBuilder::new().with_users(Arc::clone(&users) as _);
|
||||
let deps = DeleteAccountDeps {
|
||||
user: b.user_repo.clone(),
|
||||
event_publisher: Arc::new(RefusingEventPublisher),
|
||||
};
|
||||
let uid = register_user(&b, &users).await;
|
||||
|
||||
assert!(delete_account::execute(&deps, uid).await.is_err());
|
||||
}
|
||||
84
crates/application/tests/users/ensure_active_test.rs
Normal file
84
crates/application/tests/users/ensure_active_test.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
ports::UserRepository,
|
||||
testing::InMemoryStore,
|
||||
value_objects::{Email, UserId},
|
||||
};
|
||||
|
||||
use application::{
|
||||
auth::{deps::RegisterDeps, register},
|
||||
users::{deps::EnsureActiveDeps, ensure_active},
|
||||
};
|
||||
|
||||
use crate::support::{TestContextBuilder, register_command};
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_live_user_is_active() {
|
||||
let store = InMemoryStore::new();
|
||||
let b = TestContextBuilder::new().with_users(Arc::clone(&store) as _);
|
||||
let reg = RegisterDeps {
|
||||
user: b.user_repo.clone(),
|
||||
password_hasher: b.password_hasher.clone(),
|
||||
config: b.config.clone(),
|
||||
};
|
||||
register::execute(®, register_command("alice@example.com", "alice"))
|
||||
.await
|
||||
.unwrap();
|
||||
let id = store
|
||||
.find_by_email(&Email::new("alice@example.com".into()).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.id()
|
||||
.clone();
|
||||
let deps = EnsureActiveDeps {
|
||||
user: b.user_repo.clone(),
|
||||
};
|
||||
|
||||
ensure_active::execute(&deps, &id).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_deleted_user_is_rejected() {
|
||||
let store = InMemoryStore::new();
|
||||
let b = TestContextBuilder::new().with_users(Arc::clone(&store) as _);
|
||||
let reg = RegisterDeps {
|
||||
user: b.user_repo.clone(),
|
||||
password_hasher: b.password_hasher.clone(),
|
||||
config: b.config.clone(),
|
||||
};
|
||||
register::execute(®, register_command("alice@example.com", "alice"))
|
||||
.await
|
||||
.unwrap();
|
||||
let id = store
|
||||
.find_by_email(&Email::new("alice@example.com".into()).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.id()
|
||||
.clone();
|
||||
store.mark_deleted(&id, &[]).await.unwrap();
|
||||
let deps = EnsureActiveDeps {
|
||||
user: b.user_repo.clone(),
|
||||
};
|
||||
|
||||
let err = ensure_active::execute(&deps, &id).await.unwrap_err();
|
||||
|
||||
assert!(matches!(err, DomainError::Unauthorized(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_unknown_user_is_rejected() {
|
||||
let b = TestContextBuilder::new();
|
||||
let deps = EnsureActiveDeps {
|
||||
user: b.user_repo.clone(),
|
||||
};
|
||||
|
||||
let err = ensure_active::execute(&deps, &UserId::generate())
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, DomainError::Unauthorized(_)), "got {err:?}");
|
||||
}
|
||||
185
crates/application/tests/users/request_account_deletion_test.rs
Normal file
185
crates/application/tests/users/request_account_deletion_test.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
models::UserRole,
|
||||
ports::UserRepository,
|
||||
testing::InMemoryStore,
|
||||
value_objects::{Email, UserId},
|
||||
};
|
||||
|
||||
use application::{
|
||||
auth::{deps::RegisterDeps, register},
|
||||
users::{deps::RequestAccountDeletionDeps, request_account_deletion},
|
||||
};
|
||||
|
||||
use crate::support::{TestContextBuilder, register_command};
|
||||
|
||||
const PASSWORD: &str = "password123";
|
||||
|
||||
struct Fixture {
|
||||
store: Arc<InMemoryStore>,
|
||||
deps: RequestAccountDeletionDeps,
|
||||
register: RegisterDeps,
|
||||
}
|
||||
|
||||
fn fixture() -> Fixture {
|
||||
let store = InMemoryStore::new();
|
||||
let b = TestContextBuilder::new().with_users(Arc::clone(&store) as _);
|
||||
Fixture {
|
||||
deps: RequestAccountDeletionDeps {
|
||||
user: b.user_repo.clone(),
|
||||
password_hasher: b.password_hasher.clone(),
|
||||
},
|
||||
register: RegisterDeps {
|
||||
user: b.user_repo.clone(),
|
||||
password_hasher: b.password_hasher.clone(),
|
||||
config: b.config.clone(),
|
||||
},
|
||||
store,
|
||||
}
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
async fn user(&self, name: &str, role: UserRole) -> UserId {
|
||||
let email = format!("{name}@example.com");
|
||||
let mut cmd = register_command(email.clone(), name);
|
||||
cmd.role = role;
|
||||
register::execute(&self.register, cmd).await.unwrap();
|
||||
self.store
|
||||
.find_by_email(&Email::new(email).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.id()
|
||||
.clone()
|
||||
}
|
||||
|
||||
async fn is_live(&self, id: &UserId) -> bool {
|
||||
self.store.find_by_id(id).await.unwrap().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_user_deletes_their_own_account_with_the_right_password() {
|
||||
let f = fixture();
|
||||
let alice = f.user("alice", UserRole::Standard).await;
|
||||
|
||||
request_account_deletion::execute(&f.deps, alice.clone(), alice.clone(), PASSWORD)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!f.is_live(&alice).await);
|
||||
assert!(
|
||||
matches!(
|
||||
f.store.recorded_events().as_slice(),
|
||||
[DomainEvent::UserDeleted { user_id }] if *user_id == alice
|
||||
),
|
||||
"events: {:?}",
|
||||
f.store.recorded_events()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_wrong_password_is_unauthorized_and_changes_nothing() {
|
||||
let f = fixture();
|
||||
let alice = f.user("alice", UserRole::Standard).await;
|
||||
|
||||
let err = request_account_deletion::execute(&f.deps, alice.clone(), alice.clone(), "nope")
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, DomainError::Unauthorized(_)), "got {err:?}");
|
||||
assert!(f.is_live(&alice).await);
|
||||
assert!(f.store.recorded_events().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_non_admin_cannot_delete_someone_else() {
|
||||
let f = fixture();
|
||||
let alice = f.user("alice", UserRole::Standard).await;
|
||||
let bob = f.user("bob", UserRole::Standard).await;
|
||||
|
||||
let err = request_account_deletion::execute(&f.deps, alice, bob.clone(), PASSWORD)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, DomainError::Forbidden(_)), "got {err:?}");
|
||||
assert!(f.is_live(&bob).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_admin_deletes_another_user_with_their_own_password() {
|
||||
let f = fixture();
|
||||
let admin = f.user("root", UserRole::Admin).await;
|
||||
let bob = f.user("bob", UserRole::Standard).await;
|
||||
|
||||
let err = request_account_deletion::execute(&f.deps, admin.clone(), bob.clone(), "bobs-pw")
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, DomainError::Unauthorized(_)), "got {err:?}");
|
||||
assert!(f.is_live(&bob).await);
|
||||
|
||||
request_account_deletion::execute(&f.deps, admin.clone(), bob.clone(), PASSWORD)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!f.is_live(&bob).await);
|
||||
assert!(f.is_live(&admin).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn the_last_active_admin_cannot_delete_themselves() {
|
||||
let f = fixture();
|
||||
let admin = f.user("root", UserRole::Admin).await;
|
||||
|
||||
let err = request_account_deletion::execute(&f.deps, admin.clone(), admin.clone(), PASSWORD)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, DomainError::Conflict(_)), "got {err:?}");
|
||||
assert!(f.is_live(&admin).await);
|
||||
assert!(f.store.recorded_events().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_admin_can_delete_themselves_when_another_admin_remains() {
|
||||
let f = fixture();
|
||||
let first = f.user("root", UserRole::Admin).await;
|
||||
let _second = f.user("root2", UserRole::Admin).await;
|
||||
|
||||
request_account_deletion::execute(&f.deps, first.clone(), first.clone(), PASSWORD)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!f.is_live(&first).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_unknown_target_is_not_found() {
|
||||
let f = fixture();
|
||||
let admin = f.user("root", UserRole::Admin).await;
|
||||
|
||||
let err = request_account_deletion::execute(&f.deps, admin, UserId::generate(), PASSWORD)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, DomainError::NotFound(_)), "got {err:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_already_deleted_target_is_not_found() {
|
||||
let f = fixture();
|
||||
let admin = f.user("root", UserRole::Admin).await;
|
||||
let bob = f.user("bob", UserRole::Standard).await;
|
||||
request_account_deletion::execute(&f.deps, admin.clone(), bob.clone(), PASSWORD)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let err = request_account_deletion::execute(&f.deps, admin, bob, PASSWORD)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(err, DomainError::NotFound(_)), "got {err:?}");
|
||||
}
|
||||
Reference in New Issue
Block a user