mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-19 00:34:15 +00:00
Add more email-centric endpoints
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
mod common;
|
||||
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::PgPool;
|
||||
|
||||
async fn get_pool() -> PgPool {
|
||||
let conn_str = common::get_db_connection_string().await;
|
||||
sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&conn_str)
|
||||
.await
|
||||
.expect("Failed to connect to test database")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_email_success() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let pool = get_pool().await;
|
||||
|
||||
let (access_jwt, did) = common::create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({
|
||||
"recipientDid": did,
|
||||
"senderDid": "did:plc:admin",
|
||||
"content": "Hello, this is a test email from the admin.",
|
||||
"subject": "Test Admin Email"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send email");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["sent"], true);
|
||||
|
||||
let user = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("User not found");
|
||||
|
||||
let notification = sqlx::query!(
|
||||
"SELECT subject, body, notification_type as \"notification_type: String\" FROM notification_queue WHERE user_id = $1 AND notification_type = 'admin_email' ORDER BY created_at DESC LIMIT 1",
|
||||
user.id
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Notification not found");
|
||||
|
||||
assert_eq!(notification.subject.as_deref(), Some("Test Admin Email"));
|
||||
assert!(notification.body.contains("Hello, this is a test email from the admin."));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_email_default_subject() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let pool = get_pool().await;
|
||||
|
||||
let (access_jwt, did) = common::create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({
|
||||
"recipientDid": did,
|
||||
"senderDid": "did:plc:admin",
|
||||
"content": "Email without subject"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send email");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["sent"], true);
|
||||
|
||||
let user = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("User not found");
|
||||
|
||||
let notification = sqlx::query!(
|
||||
"SELECT subject FROM notification_queue WHERE user_id = $1 AND notification_type = 'admin_email' AND body = 'Email without subject' LIMIT 1",
|
||||
user.id
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Notification not found");
|
||||
|
||||
assert!(notification.subject.is_some());
|
||||
assert!(notification.subject.unwrap().contains("Message from"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_email_recipient_not_found() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
|
||||
let (access_jwt, _) = common::create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({
|
||||
"recipientDid": "did:plc:nonexistent",
|
||||
"senderDid": "did:plc:admin",
|
||||
"content": "Test content"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send email");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["error"], "AccountNotFound");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_email_missing_content() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
|
||||
let (access_jwt, did) = common::create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({
|
||||
"recipientDid": did,
|
||||
"senderDid": "did:plc:admin",
|
||||
"content": ""
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send email");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["error"], "InvalidRequest");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_email_missing_recipient() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
|
||||
let (access_jwt, _) = common::create_account_and_login(&client).await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url))
|
||||
.bearer_auth(&access_jwt)
|
||||
.json(&json!({
|
||||
"recipientDid": "",
|
||||
"senderDid": "did:plc:admin",
|
||||
"content": "Test content"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send email");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["error"], "InvalidRequest");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_email_requires_auth() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url))
|
||||
.json(&json!({
|
||||
"recipientDid": "did:plc:test",
|
||||
"senderDid": "did:plc:admin",
|
||||
"content": "Test content"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send email");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
mod common;
|
||||
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::PgPool;
|
||||
|
||||
async fn get_pool() -> PgPool {
|
||||
let conn_str = common::get_db_connection_string().await;
|
||||
sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&conn_str)
|
||||
.await
|
||||
.expect("Failed to connect to test database")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_password_reset_creates_code() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let pool = get_pool().await;
|
||||
|
||||
let handle = format!("pwreset_{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
let payload = json!({
|
||||
"handle": handle,
|
||||
"email": email,
|
||||
"password": "oldpassword"
|
||||
});
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create account");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.requestPasswordReset", base_url))
|
||||
.json(&json!({"email": email}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request password reset");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let user = sqlx::query!(
|
||||
"SELECT password_reset_code, password_reset_code_expires_at FROM users WHERE email = $1",
|
||||
email
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("User not found");
|
||||
|
||||
assert!(user.password_reset_code.is_some());
|
||||
assert!(user.password_reset_code_expires_at.is_some());
|
||||
|
||||
let code = user.password_reset_code.unwrap();
|
||||
assert!(code.contains('-'));
|
||||
assert_eq!(code.len(), 11);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_password_reset_unknown_email_returns_ok() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.requestPasswordReset", base_url))
|
||||
.json(&json!({"email": "nonexistent@example.com"}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request password reset");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reset_password_with_valid_token() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let pool = get_pool().await;
|
||||
|
||||
let handle = format!("pwreset2_{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
let old_password = "oldpassword";
|
||||
let new_password = "newpassword123";
|
||||
|
||||
let payload = json!({
|
||||
"handle": handle,
|
||||
"email": email,
|
||||
"password": old_password
|
||||
});
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create account");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.requestPasswordReset", base_url))
|
||||
.json(&json!({"email": email}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request password reset");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let user = sqlx::query!(
|
||||
"SELECT password_reset_code FROM users WHERE email = $1",
|
||||
email
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("User not found");
|
||||
|
||||
let token = user.password_reset_code.expect("No reset code");
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.resetPassword", base_url))
|
||||
.json(&json!({
|
||||
"token": token,
|
||||
"password": new_password
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to reset password");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let user = sqlx::query!(
|
||||
"SELECT password_reset_code, password_reset_code_expires_at FROM users WHERE email = $1",
|
||||
email
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("User not found");
|
||||
assert!(user.password_reset_code.is_none());
|
||||
assert!(user.password_reset_code_expires_at.is_none());
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.createSession", base_url))
|
||||
.json(&json!({
|
||||
"identifier": handle,
|
||||
"password": new_password
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to login");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.createSession", base_url))
|
||||
.json(&json!({
|
||||
"identifier": handle,
|
||||
"password": old_password
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to login attempt");
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reset_password_with_invalid_token() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.resetPassword", base_url))
|
||||
.json(&json!({
|
||||
"token": "invalid-token",
|
||||
"password": "newpassword"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to reset password");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["error"], "InvalidToken");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reset_password_with_expired_token() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let pool = get_pool().await;
|
||||
|
||||
let handle = format!("pwreset3_{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
|
||||
let payload = json!({
|
||||
"handle": handle,
|
||||
"email": email,
|
||||
"password": "oldpassword"
|
||||
});
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create account");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.requestPasswordReset", base_url))
|
||||
.json(&json!({"email": email}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request password reset");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let user = sqlx::query!(
|
||||
"SELECT password_reset_code FROM users WHERE email = $1",
|
||||
email
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("User not found");
|
||||
|
||||
let token = user.password_reset_code.expect("No reset code");
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE users SET password_reset_code_expires_at = NOW() - INTERVAL '1 hour' WHERE email = $1",
|
||||
email
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("Failed to expire token");
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.resetPassword", base_url))
|
||||
.json(&json!({
|
||||
"token": token,
|
||||
"password": "newpassword"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to reset password");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["error"], "ExpiredToken");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reset_password_invalidates_sessions() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
let pool = get_pool().await;
|
||||
|
||||
let handle = format!("pwreset4_{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
|
||||
let payload = json!({
|
||||
"handle": handle,
|
||||
"email": email,
|
||||
"password": "oldpassword"
|
||||
});
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create account");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
let original_token = body["accessJwt"].as_str().expect("No accessJwt").to_string();
|
||||
|
||||
let res = client
|
||||
.get(format!("{}/xrpc/com.atproto.server.getSession", base_url))
|
||||
.bearer_auth(&original_token)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to get session");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.requestPasswordReset", base_url))
|
||||
.json(&json!({"email": email}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request password reset");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let user = sqlx::query!(
|
||||
"SELECT password_reset_code FROM users WHERE email = $1",
|
||||
email
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("User not found");
|
||||
|
||||
let token = user.password_reset_code.expect("No reset code");
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.resetPassword", base_url))
|
||||
.json(&json!({
|
||||
"token": token,
|
||||
"password": "newpassword123"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to reset password");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let res = client
|
||||
.get(format!("{}/xrpc/com.atproto.server.getSession", base_url))
|
||||
.bearer_auth(&original_token)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to get session");
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_password_reset_empty_email() {
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.requestPasswordReset", base_url))
|
||||
.json(&json!({"email": ""}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request password reset");
|
||||
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: Value = res.json().await.expect("Invalid JSON");
|
||||
assert_eq!(body["error"], "InvalidRequest");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reset_password_creates_notification() {
|
||||
let pool = get_pool().await;
|
||||
let client = common::client();
|
||||
let base_url = common::base_url().await;
|
||||
|
||||
let handle = format!("pwreset5_{}", uuid::Uuid::new_v4());
|
||||
let email = format!("{}@example.com", handle);
|
||||
|
||||
let payload = json!({
|
||||
"handle": handle,
|
||||
"email": email,
|
||||
"password": "oldpassword"
|
||||
});
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create account");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let user = sqlx::query!("SELECT id FROM users WHERE email = $1", email)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("User not found");
|
||||
|
||||
let initial_count: i64 = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM notification_queue WHERE user_id = $1 AND notification_type = 'password_reset'",
|
||||
user.id
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Failed to count")
|
||||
.unwrap_or(0);
|
||||
|
||||
let res = client
|
||||
.post(format!("{}/xrpc/com.atproto.server.requestPasswordReset", base_url))
|
||||
.json(&json!({"email": email}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to request password reset");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let final_count: i64 = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM notification_queue WHERE user_id = $1 AND notification_type = 'password_reset'",
|
||||
user.id
|
||||
)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("Failed to count")
|
||||
.unwrap_or(0);
|
||||
|
||||
assert_eq!(final_count - initial_count, 1);
|
||||
}
|
||||
Reference in New Issue
Block a user