Updated TODO with more stuff, added user delete endpoint

This commit is contained in:
lewis
2025-12-10 17:22:22 +02:00
parent f19dac454e
commit 44c3d4129e
11 changed files with 891 additions and 2 deletions

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM account_deletion_requests WHERE token = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "4018f515492ac54792fe7d44e6e708b00ae7f33970a63090a816327d698b1d14"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM app_passwords WHERE user_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "5cbad5f679e8a1abe945778e3edd0b489a39b4ba5a1609ac1a256dc58c829419"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM account_deletion_requests WHERE did = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "6aef2838d99c33014f59a5d315563426183d66d802a42698e8ad5f0e2c326ecc"
}

View File

@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, password_hash FROM users WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "password_hash",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "76c6ef1d5395105a0cdedb27ca321c9e3eae1ce87c223b706ed81ebf973875f3"
}

View File

@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT did, expires_at FROM account_deletion_requests WHERE token = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "expires_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "a33d91e53a9284a8d7c1dbd072bf6177e8734c28b9bb110250d700e14b0263d1"
}

57
TODO.md
View File

@@ -31,7 +31,8 @@ Lewis' corrected big boy todofile
- [x] Implement `com.atproto.server.createAppPassword`.
- [x] Implement `com.atproto.server.createInviteCode`.
- [x] Implement `com.atproto.server.createInviteCodes`.
- [x] Implement `com.atproto.server.deactivateAccount` / `deleteAccount`.
- [x] Implement `com.atproto.server.deactivateAccount`.
- [x] Implement `com.atproto.server.deleteAccount` (user-initiated, requires password + email token).
- [x] Implement `com.atproto.server.getAccountInviteCodes`.
- [x] Implement `com.atproto.server.getServiceAuth` (Cross-service auth).
- [x] Implement `com.atproto.server.listAppPasswords`.
@@ -106,9 +107,63 @@ Lewis' corrected big boy todofile
## Moderation (`com.atproto.moderation`)
- [x] Implement `com.atproto.moderation.createReport`.
## Temp Namespace (`com.atproto.temp`)
- [ ] Implement `com.atproto.temp.checkSignupQueue` (signup queue status for gated signups).
## OAuth 2.0 Support
The reference PDS implements full OAuth 2.0 provider functionality for native app authentication.
- [ ] OAuth Provider Core
- [ ] Implement `/.well-known/oauth-protected-resource` metadata endpoint.
- [ ] Implement `/.well-known/oauth-authorization-server` metadata endpoint.
- [ ] Implement `/oauth/authorize` authorization endpoint.
- [ ] Implement `/oauth/par` Pushed Authorization Request endpoint.
- [ ] Implement `/oauth/token` token endpoint.
- [ ] Implement `/oauth/jwks` JSON Web Key Set endpoint.
- [ ] OAuth Database Tables
- [ ] Device table for tracking authorized devices.
- [ ] Authorization request table.
- [ ] Authorized client table.
- [ ] Token table for OAuth tokens.
- [ ] Used refresh token table.
- [ ] DPoP (Demonstrating Proof-of-Possession) support.
- [ ] Client metadata fetching and validation.
## PDS-Level App Endpoints
These endpoints need to be implemented at the PDS level (not just proxied to appview).
### Actor (`app.bsky.actor`)
- [ ] Implement `app.bsky.actor.getPreferences` (user preferences storage).
- [ ] Implement `app.bsky.actor.putPreferences` (update user preferences).
- [ ] Implement `app.bsky.actor.getProfile` (PDS-level with proxy fallback).
- [ ] Implement `app.bsky.actor.getProfiles` (PDS-level with proxy fallback).
### Feed (`app.bsky.feed`)
These are implemented at PDS level to enable local-first reads:
- [ ] Implement `app.bsky.feed.getTimeline` (PDS-level with proxy).
- [ ] Implement `app.bsky.feed.getAuthorFeed` (PDS-level with proxy).
- [ ] Implement `app.bsky.feed.getActorLikes` (PDS-level with proxy).
- [ ] Implement `app.bsky.feed.getPostThread` (PDS-level with proxy).
- [ ] Implement `app.bsky.feed.getFeed` (PDS-level with proxy).
### Notification (`app.bsky.notification`)
- [ ] Implement `app.bsky.notification.registerPush` (push notification registration).
## Deprecated Sync Endpoints (for compatibility)
- [ ] Implement `com.atproto.sync.getCheckout` (deprecated, still needed for compatibility).
- [ ] Implement `com.atproto.sync.getHead` (deprecated, still needed for compatibility).
## Misc HTTP Endpoints
- [ ] Implement `/robots.txt` endpoint.
## Record Schema Validation
- [ ] Handle this generically.
## Preference Storage
User preferences (for app.bsky.actor.getPreferences/putPreferences):
- [ ] Create preferences table for storing user app preferences.
- [ ] Implement `app.bsky.actor.getPreferences` handler (read from postgres, proxy fallback).
- [ ] Implement `app.bsky.actor.putPreferences` handler (write to postgres).
## Infrastructure & Core Components
- [x] Sequencer (Event Log)
- [x] Implement a `Sequencer` (backed by `repo_seq` table).

View File

@@ -5,6 +5,7 @@ use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use bcrypt::verify;
use chrono::{Duration, Utc};
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -391,3 +392,211 @@ pub async fn request_account_delete(
(StatusCode::OK, Json(json!({}))).into_response()
}
#[derive(Deserialize)]
pub struct DeleteAccountInput {
pub did: String,
pub password: String,
pub token: String,
}
pub async fn delete_account(
State(state): State<AppState>,
Json(input): Json<DeleteAccountInput>,
) -> Response {
let did = input.did.trim();
let password = &input.password;
let token = input.token.trim();
if did.is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "did is required"})),
)
.into_response();
}
if password.is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidRequest", "message": "password is required"})),
)
.into_response();
}
if token.is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidToken", "message": "token is required"})),
)
.into_response();
}
let user = sqlx::query!(
"SELECT id, password_hash FROM users WHERE did = $1",
did
)
.fetch_optional(&state.db)
.await;
let (user_id, password_hash) = match user {
Ok(Some(row)) => (row.id, row.password_hash),
Ok(None) => {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
)
.into_response();
}
Err(e) => {
error!("DB error in delete_account: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
let password_valid = if verify(password, &password_hash).unwrap_or(false) {
true
} else {
let app_pass_rows = sqlx::query!(
"SELECT password_hash FROM app_passwords WHERE user_id = $1",
user_id
)
.fetch_all(&state.db)
.await
.unwrap_or_default();
app_pass_rows
.iter()
.any(|row| verify(password, &row.password_hash).unwrap_or(false))
};
if !password_valid {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed", "message": "Invalid password"})),
)
.into_response();
}
let deletion_request = sqlx::query!(
"SELECT did, expires_at FROM account_deletion_requests WHERE token = $1",
token
)
.fetch_optional(&state.db)
.await;
let (token_did, expires_at) = match deletion_request {
Ok(Some(row)) => (row.did, row.expires_at),
Ok(None) => {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})),
)
.into_response();
}
Err(e) => {
error!("DB error fetching deletion token: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
if token_did != did {
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "InvalidToken", "message": "Token does not match account"})),
)
.into_response();
}
if Utc::now() > expires_at {
let _ = sqlx::query!("DELETE FROM account_deletion_requests WHERE token = $1", token)
.execute(&state.db)
.await;
return (
StatusCode::BAD_REQUEST,
Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
)
.into_response();
}
let mut tx = match state.db.begin().await {
Ok(tx) => tx,
Err(e) => {
error!("Failed to begin transaction: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
let deletion_result: Result<(), sqlx::Error> = async {
sqlx::query!("DELETE FROM sessions WHERE did = $1", did)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM user_keys WHERE user_id = $1", user_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1", user_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM account_deletion_requests WHERE did = $1", did)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM users WHERE id = $1", user_id)
.execute(&mut *tx)
.await?;
Ok(())
}
.await;
match deletion_result {
Ok(()) => {
if let Err(e) = tx.commit().await {
error!("Failed to commit account deletion transaction: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
info!("Account {} deleted successfully", did);
(StatusCode::OK, Json(json!({}))).into_response()
}
Err(e) => {
error!("DB error deleting account, rolling back: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response()
}
}
}

View File

@@ -8,7 +8,8 @@ pub mod session;
pub mod signing_key;
pub use account_status::{
activate_account, check_account_status, deactivate_account, request_account_delete,
activate_account, check_account_status, deactivate_account, delete_account,
request_account_delete,
};
pub use app_password::{create_app_password, list_app_passwords, revoke_app_password};
pub use email::{confirm_email, request_email_update, update_email};

View File

@@ -160,6 +160,10 @@ pub fn app(state: AppState) -> Router {
"/xrpc/com.atproto.server.requestAccountDelete",
post(api::server::request_account_delete),
)
.route(
"/xrpc/com.atproto.server.deleteAccount",
post(api::server::delete_account),
)
.route(
"/xrpc/com.atproto.server.requestPasswordReset",
post(api::server::request_password_reset),

520
tests/delete_account.rs Normal file
View File

@@ -0,0 +1,520 @@
mod common;
mod helpers;
use common::*;
use chrono::Utc;
use reqwest::StatusCode;
use serde_json::{Value, json};
#[tokio::test]
async fn test_delete_account_full_flow() {
let client = client();
let ts = Utc::now().timestamp_millis();
let handle = format!("delete-test-{}.test", ts);
let email = format!("delete-test-{}@test.com", ts);
let password = "delete-password-123";
let create_payload = json!({
"handle": handle,
"email": email,
"password": password
});
let create_res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&create_payload)
.send()
.await
.expect("Failed to create account");
assert_eq!(create_res.status(), StatusCode::OK);
let create_body: Value = create_res.json().await.unwrap();
let did = create_body["did"].as_str().unwrap().to_string();
let jwt = create_body["accessJwt"].as_str().unwrap().to_string();
let request_delete_res = client
.post(format!(
"{}/xrpc/com.atproto.server.requestAccountDelete",
base_url().await
))
.bearer_auth(&jwt)
.send()
.await
.expect("Failed to request account deletion");
assert_eq!(request_delete_res.status(), StatusCode::OK);
let db_url = get_db_connection_string().await;
let pool = sqlx::PgPool::connect(&db_url).await.expect("Failed to connect to test DB");
let row = sqlx::query!("SELECT token FROM account_deletion_requests WHERE did = $1", did)
.fetch_one(&pool)
.await
.expect("Failed to query deletion token");
let token = row.token;
let delete_payload = json!({
"did": did,
"password": password,
"token": token
});
let delete_res = client
.post(format!(
"{}/xrpc/com.atproto.server.deleteAccount",
base_url().await
))
.json(&delete_payload)
.send()
.await
.expect("Failed to delete account");
assert_eq!(delete_res.status(), StatusCode::OK);
let user_row = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
.fetch_optional(&pool)
.await
.expect("Failed to query user");
assert!(user_row.is_none(), "User should be deleted from database");
let session_res = client
.get(format!(
"{}/xrpc/com.atproto.server.getSession",
base_url().await
))
.bearer_auth(&jwt)
.send()
.await
.expect("Failed to check session");
assert_eq!(session_res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_delete_account_wrong_password() {
let client = client();
let ts = Utc::now().timestamp_millis();
let handle = format!("delete-wrongpw-{}.test", ts);
let email = format!("delete-wrongpw-{}@test.com", ts);
let password = "correct-password";
let create_payload = json!({
"handle": handle,
"email": email,
"password": password
});
let create_res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&create_payload)
.send()
.await
.expect("Failed to create account");
assert_eq!(create_res.status(), StatusCode::OK);
let create_body: Value = create_res.json().await.unwrap();
let did = create_body["did"].as_str().unwrap().to_string();
let jwt = create_body["accessJwt"].as_str().unwrap().to_string();
let request_delete_res = client
.post(format!(
"{}/xrpc/com.atproto.server.requestAccountDelete",
base_url().await
))
.bearer_auth(&jwt)
.send()
.await
.expect("Failed to request account deletion");
assert_eq!(request_delete_res.status(), StatusCode::OK);
let db_url = get_db_connection_string().await;
let pool = sqlx::PgPool::connect(&db_url).await.expect("Failed to connect to test DB");
let row = sqlx::query!("SELECT token FROM account_deletion_requests WHERE did = $1", did)
.fetch_one(&pool)
.await
.expect("Failed to query deletion token");
let token = row.token;
let delete_payload = json!({
"did": did,
"password": "wrong-password",
"token": token
});
let delete_res = client
.post(format!(
"{}/xrpc/com.atproto.server.deleteAccount",
base_url().await
))
.json(&delete_payload)
.send()
.await
.expect("Failed to send delete request");
assert_eq!(delete_res.status(), StatusCode::UNAUTHORIZED);
let body: Value = delete_res.json().await.unwrap();
assert_eq!(body["error"], "AuthenticationFailed");
}
#[tokio::test]
async fn test_delete_account_invalid_token() {
let client = client();
let ts = Utc::now().timestamp_millis();
let handle = format!("delete-badtoken-{}.test", ts);
let email = format!("delete-badtoken-{}@test.com", ts);
let password = "delete-password";
let create_payload = json!({
"handle": handle,
"email": email,
"password": password
});
let create_res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&create_payload)
.send()
.await
.expect("Failed to create account");
assert_eq!(create_res.status(), StatusCode::OK);
let create_body: Value = create_res.json().await.unwrap();
let did = create_body["did"].as_str().unwrap().to_string();
let delete_payload = json!({
"did": did,
"password": password,
"token": "invalid-token-12345"
});
let delete_res = client
.post(format!(
"{}/xrpc/com.atproto.server.deleteAccount",
base_url().await
))
.json(&delete_payload)
.send()
.await
.expect("Failed to send delete request");
assert_eq!(delete_res.status(), StatusCode::BAD_REQUEST);
let body: Value = delete_res.json().await.unwrap();
assert_eq!(body["error"], "InvalidToken");
}
#[tokio::test]
async fn test_delete_account_expired_token() {
let client = client();
let ts = Utc::now().timestamp_millis();
let handle = format!("delete-expired-{}.test", ts);
let email = format!("delete-expired-{}@test.com", ts);
let password = "delete-password";
let create_payload = json!({
"handle": handle,
"email": email,
"password": password
});
let create_res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&create_payload)
.send()
.await
.expect("Failed to create account");
assert_eq!(create_res.status(), StatusCode::OK);
let create_body: Value = create_res.json().await.unwrap();
let did = create_body["did"].as_str().unwrap().to_string();
let jwt = create_body["accessJwt"].as_str().unwrap().to_string();
let request_delete_res = client
.post(format!(
"{}/xrpc/com.atproto.server.requestAccountDelete",
base_url().await
))
.bearer_auth(&jwt)
.send()
.await
.expect("Failed to request account deletion");
assert_eq!(request_delete_res.status(), StatusCode::OK);
let db_url = get_db_connection_string().await;
let pool = sqlx::PgPool::connect(&db_url).await.expect("Failed to connect to test DB");
let row = sqlx::query!("SELECT token FROM account_deletion_requests WHERE did = $1", did)
.fetch_one(&pool)
.await
.expect("Failed to query deletion token");
let token = row.token;
sqlx::query!(
"UPDATE account_deletion_requests SET expires_at = NOW() - INTERVAL '1 hour' WHERE token = $1",
token
)
.execute(&pool)
.await
.expect("Failed to expire token");
let delete_payload = json!({
"did": did,
"password": password,
"token": token
});
let delete_res = client
.post(format!(
"{}/xrpc/com.atproto.server.deleteAccount",
base_url().await
))
.json(&delete_payload)
.send()
.await
.expect("Failed to send delete request");
assert_eq!(delete_res.status(), StatusCode::BAD_REQUEST);
let body: Value = delete_res.json().await.unwrap();
assert_eq!(body["error"], "ExpiredToken");
}
#[tokio::test]
async fn test_delete_account_token_mismatch() {
let client = client();
let ts = Utc::now().timestamp_millis();
let handle1 = format!("delete-user1-{}.test", ts);
let email1 = format!("delete-user1-{}@test.com", ts);
let password1 = "user1-password";
let create1_res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&json!({
"handle": handle1,
"email": email1,
"password": password1
}))
.send()
.await
.expect("Failed to create account 1");
assert_eq!(create1_res.status(), StatusCode::OK);
let create1_body: Value = create1_res.json().await.unwrap();
let did1 = create1_body["did"].as_str().unwrap().to_string();
let jwt1 = create1_body["accessJwt"].as_str().unwrap().to_string();
let handle2 = format!("delete-user2-{}.test", ts);
let email2 = format!("delete-user2-{}@test.com", ts);
let password2 = "user2-password";
let create2_res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&json!({
"handle": handle2,
"email": email2,
"password": password2
}))
.send()
.await
.expect("Failed to create account 2");
assert_eq!(create2_res.status(), StatusCode::OK);
let create2_body: Value = create2_res.json().await.unwrap();
let did2 = create2_body["did"].as_str().unwrap().to_string();
let request_delete_res = client
.post(format!(
"{}/xrpc/com.atproto.server.requestAccountDelete",
base_url().await
))
.bearer_auth(&jwt1)
.send()
.await
.expect("Failed to request account deletion");
assert_eq!(request_delete_res.status(), StatusCode::OK);
let db_url = get_db_connection_string().await;
let pool = sqlx::PgPool::connect(&db_url).await.expect("Failed to connect to test DB");
let row = sqlx::query!("SELECT token FROM account_deletion_requests WHERE did = $1", did1)
.fetch_one(&pool)
.await
.expect("Failed to query deletion token");
let token = row.token;
let delete_payload = json!({
"did": did2,
"password": password2,
"token": token
});
let delete_res = client
.post(format!(
"{}/xrpc/com.atproto.server.deleteAccount",
base_url().await
))
.json(&delete_payload)
.send()
.await
.expect("Failed to send delete request");
assert_eq!(delete_res.status(), StatusCode::BAD_REQUEST);
let body: Value = delete_res.json().await.unwrap();
assert_eq!(body["error"], "InvalidToken");
}
#[tokio::test]
async fn test_delete_account_with_app_password() {
let client = client();
let ts = Utc::now().timestamp_millis();
let handle = format!("delete-apppw-{}.test", ts);
let email = format!("delete-apppw-{}@test.com", ts);
let main_password = "main-password-123";
let create_payload = json!({
"handle": handle,
"email": email,
"password": main_password
});
let create_res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&create_payload)
.send()
.await
.expect("Failed to create account");
assert_eq!(create_res.status(), StatusCode::OK);
let create_body: Value = create_res.json().await.unwrap();
let did = create_body["did"].as_str().unwrap().to_string();
let jwt = create_body["accessJwt"].as_str().unwrap().to_string();
let app_password_res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAppPassword",
base_url().await
))
.bearer_auth(&jwt)
.json(&json!({ "name": "delete-test-app" }))
.send()
.await
.expect("Failed to create app password");
assert_eq!(app_password_res.status(), StatusCode::OK);
let app_password_body: Value = app_password_res.json().await.unwrap();
let app_password = app_password_body["password"].as_str().unwrap().to_string();
let request_delete_res = client
.post(format!(
"{}/xrpc/com.atproto.server.requestAccountDelete",
base_url().await
))
.bearer_auth(&jwt)
.send()
.await
.expect("Failed to request account deletion");
assert_eq!(request_delete_res.status(), StatusCode::OK);
let db_url = get_db_connection_string().await;
let pool = sqlx::PgPool::connect(&db_url).await.expect("Failed to connect to test DB");
let row = sqlx::query!("SELECT token FROM account_deletion_requests WHERE did = $1", did)
.fetch_one(&pool)
.await
.expect("Failed to query deletion token");
let token = row.token;
let delete_payload = json!({
"did": did,
"password": app_password,
"token": token
});
let delete_res = client
.post(format!(
"{}/xrpc/com.atproto.server.deleteAccount",
base_url().await
))
.json(&delete_payload)
.send()
.await
.expect("Failed to delete account");
assert_eq!(delete_res.status(), StatusCode::OK);
let user_row = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
.fetch_optional(&pool)
.await
.expect("Failed to query user");
assert!(user_row.is_none(), "User should be deleted from database");
}
#[tokio::test]
async fn test_delete_account_missing_fields() {
let client = client();
let res1 = client
.post(format!(
"{}/xrpc/com.atproto.server.deleteAccount",
base_url().await
))
.json(&json!({
"password": "test",
"token": "test"
}))
.send()
.await
.expect("Failed to send request");
assert_eq!(res1.status(), StatusCode::UNPROCESSABLE_ENTITY);
let res2 = client
.post(format!(
"{}/xrpc/com.atproto.server.deleteAccount",
base_url().await
))
.json(&json!({
"did": "did:web:test",
"token": "test"
}))
.send()
.await
.expect("Failed to send request");
assert_eq!(res2.status(), StatusCode::UNPROCESSABLE_ENTITY);
let res3 = client
.post(format!(
"{}/xrpc/com.atproto.server.deleteAccount",
base_url().await
))
.json(&json!({
"did": "did:web:test",
"password": "test"
}))
.send()
.await
.expect("Failed to send request");
assert_eq!(res3.status(), StatusCode::UNPROCESSABLE_ENTITY);
}
#[tokio::test]
async fn test_delete_account_nonexistent_user() {
let client = client();
let delete_payload = json!({
"did": "did:web:nonexistent.user",
"password": "any-password",
"token": "any-token"
});
let delete_res = client
.post(format!(
"{}/xrpc/com.atproto.server.deleteAccount",
base_url().await
))
.json(&delete_payload)
.send()
.await
.expect("Failed to send delete request");
assert_eq!(delete_res.status(), StatusCode::BAD_REQUEST);
let body: Value = delete_res.json().await.unwrap();
assert_eq!(body["error"], "AccountNotFound");
}

View File

@@ -4,6 +4,7 @@ use serde_json::{Value, json};
pub use crate::common::*;
#[allow(dead_code)]
pub async fn setup_new_user(handle_prefix: &str) -> (String, String) {
let client = client();
let ts = Utc::now().timestamp_millis();
@@ -50,6 +51,7 @@ pub async fn setup_new_user(handle_prefix: &str) -> (String, String) {
(new_did, new_jwt)
}
#[allow(dead_code)]
pub async fn create_post(
client: &reqwest::Client,
did: &str,