use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::user_store::{UserStore, UserStoreError};
pub const CONFIRMATION_PHRASE: &str = "DELETE MY ACCOUNT";
#[derive(Debug, Error)]
pub enum AccountDeleteError {
#[error("confirmation mismatch: expected \"{expected}\"")]
ConfirmationMismatch { expected: &'static str },
#[error("user not found")]
NotFound,
#[error("not implemented by this store")]
NotImplemented,
#[error("user store: {0}")]
UserStore(String),
}
#[derive(Debug, Deserialize)]
pub struct AccountDeleteRequest {
pub confirmation: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct AccountDeleteResponse {
pub message: String,
}
pub async fn delete_account(
user_id: &str,
req: &AccountDeleteRequest,
user_store: &dyn UserStore,
) -> Result<AccountDeleteResponse, AccountDeleteError> {
if req.confirmation != CONFIRMATION_PHRASE {
return Err(AccountDeleteError::ConfirmationMismatch {
expected: CONFIRMATION_PHRASE,
});
}
let deleted = user_store.delete(user_id).await.map_err(|e| match e {
UserStoreError::NotImplemented => AccountDeleteError::NotImplemented,
other => AccountDeleteError::UserStore(other.to_string()),
})?;
if !deleted {
return Err(AccountDeleteError::NotFound);
}
Ok(AccountDeleteResponse {
message: "account deleted".into(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::user_store::InMemoryUserStore;
fn seed() -> InMemoryUserStore {
let store = InMemoryUserStore::new();
store
.insert_user(
"acct-del",
"del@example.com",
"https://del.example/profile#me",
None,
"password123",
)
.unwrap();
store
}
#[tokio::test]
async fn delete_account_succeeds_with_correct_confirmation() {
let store = seed();
let req = AccountDeleteRequest {
confirmation: "DELETE MY ACCOUNT".into(),
};
let resp = delete_account("acct-del", &req, &store).await.unwrap();
assert_eq!(resp.message, "account deleted");
assert!(store.find_by_id("acct-del").await.unwrap().is_none());
}
#[tokio::test]
async fn delete_account_rejects_wrong_confirmation() {
let store = seed();
let req = AccountDeleteRequest {
confirmation: "delete my account".into(), };
let err = delete_account("acct-del", &req, &store).await.unwrap_err();
assert!(matches!(
err,
AccountDeleteError::ConfirmationMismatch { .. }
));
assert!(store.find_by_id("acct-del").await.unwrap().is_some());
}
#[tokio::test]
async fn delete_account_rejects_empty_confirmation() {
let store = seed();
let req = AccountDeleteRequest {
confirmation: "".into(),
};
let err = delete_account("acct-del", &req, &store).await.unwrap_err();
assert!(matches!(
err,
AccountDeleteError::ConfirmationMismatch { .. }
));
}
#[tokio::test]
async fn delete_account_returns_not_found_for_unknown_user() {
let store = seed();
let req = AccountDeleteRequest {
confirmation: "DELETE MY ACCOUNT".into(),
};
let err = delete_account("nonexistent", &req, &store)
.await
.unwrap_err();
assert!(matches!(err, AccountDeleteError::NotFound));
}
#[tokio::test]
async fn delete_account_idempotent() {
let store = seed();
let req = AccountDeleteRequest {
confirmation: "DELETE MY ACCOUNT".into(),
};
delete_account("acct-del", &req, &store).await.unwrap();
let err = delete_account("acct-del", &req, &store).await.unwrap_err();
assert!(matches!(err, AccountDeleteError::NotFound));
}
}