Files
tranquil-pds/tests/plc_operations.rs
T

430 lines
14 KiB
Rust

mod common;
use common::*;
use reqwest::StatusCode;
use serde_json::json;
use sqlx::PgPool;
#[tokio::test]
async fn test_request_plc_operation_signature_requires_auth() {
let client = client();
let res = client
.post(format!(
"{}/xrpc/com.atproto.identity.requestPlcOperationSignature",
base_url().await
))
.send()
.await
.expect("Request failed");
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_request_plc_operation_signature_success() {
let client = client();
let (token, _did) = create_account_and_login(&client).await;
let res = client
.post(format!(
"{}/xrpc/com.atproto.identity.requestPlcOperationSignature",
base_url().await
))
.bearer_auth(&token)
.send()
.await
.expect("Request failed");
assert_eq!(res.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_sign_plc_operation_requires_auth() {
let client = client();
let res = client
.post(format!(
"{}/xrpc/com.atproto.identity.signPlcOperation",
base_url().await
))
.json(&json!({}))
.send()
.await
.expect("Request failed");
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_sign_plc_operation_requires_token() {
let client = client();
let (token, _did) = create_account_and_login(&client).await;
let res = client
.post(format!(
"{}/xrpc/com.atproto.identity.signPlcOperation",
base_url().await
))
.bearer_auth(&token)
.json(&json!({}))
.send()
.await
.expect("Request failed");
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let body: serde_json::Value = res.json().await.unwrap();
assert_eq!(body["error"], "InvalidRequest");
}
#[tokio::test]
async fn test_sign_plc_operation_invalid_token() {
let client = client();
let (token, _did) = create_account_and_login(&client).await;
let res = client
.post(format!(
"{}/xrpc/com.atproto.identity.signPlcOperation",
base_url().await
))
.bearer_auth(&token)
.json(&json!({
"token": "invalid-token-12345"
}))
.send()
.await
.expect("Request failed");
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let body: serde_json::Value = res.json().await.unwrap();
assert!(body["error"] == "InvalidToken" || body["error"] == "ExpiredToken");
}
#[tokio::test]
async fn test_submit_plc_operation_requires_auth() {
let client = client();
let res = client
.post(format!(
"{}/xrpc/com.atproto.identity.submitPlcOperation",
base_url().await
))
.json(&json!({
"operation": {}
}))
.send()
.await
.expect("Request failed");
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_submit_plc_operation_invalid_operation() {
let client = client();
let (token, _did) = create_account_and_login(&client).await;
let res = client
.post(format!(
"{}/xrpc/com.atproto.identity.submitPlcOperation",
base_url().await
))
.bearer_auth(&token)
.json(&json!({
"operation": {
"type": "invalid_type"
}
}))
.send()
.await
.expect("Request failed");
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let body: serde_json::Value = res.json().await.unwrap();
assert_eq!(body["error"], "InvalidRequest");
}
#[tokio::test]
async fn test_submit_plc_operation_missing_sig() {
let client = client();
let (token, _did) = create_account_and_login(&client).await;
let res = client
.post(format!(
"{}/xrpc/com.atproto.identity.submitPlcOperation",
base_url().await
))
.bearer_auth(&token)
.json(&json!({
"operation": {
"type": "plc_operation",
"rotationKeys": [],
"verificationMethods": {},
"alsoKnownAs": [],
"services": {},
"prev": null
}
}))
.send()
.await
.expect("Request failed");
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let body: serde_json::Value = res.json().await.unwrap();
assert_eq!(body["error"], "InvalidRequest");
}
#[tokio::test]
async fn test_submit_plc_operation_wrong_service_endpoint() {
let client = client();
let (token, _did) = create_account_and_login(&client).await;
let res = client
.post(format!(
"{}/xrpc/com.atproto.identity.submitPlcOperation",
base_url().await
))
.bearer_auth(&token)
.json(&json!({
"operation": {
"type": "plc_operation",
"rotationKeys": ["did:key:z123"],
"verificationMethods": {"atproto": "did:key:z456"},
"alsoKnownAs": ["at://wrong.handle"],
"services": {
"atproto_pds": {
"type": "AtprotoPersonalDataServer",
"endpoint": "https://wrong.example.com"
}
},
"prev": null,
"sig": "fake_signature"
}
}))
.send()
.await
.expect("Request failed");
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn test_request_plc_operation_creates_token_in_db() {
let client = client();
let (token, did) = create_account_and_login(&client).await;
let res = client
.post(format!(
"{}/xrpc/com.atproto.identity.requestPlcOperationSignature",
base_url().await
))
.bearer_auth(&token)
.send()
.await
.expect("Request failed");
assert_eq!(res.status(), StatusCode::OK);
let db_url = get_db_connection_string().await;
let pool = PgPool::connect(&db_url).await.expect("DB connect failed");
let row = sqlx::query!(
r#"
SELECT t.token, t.expires_at
FROM plc_operation_tokens t
JOIN users u ON t.user_id = u.id
WHERE u.did = $1
"#,
did
)
.fetch_optional(&pool)
.await
.expect("Query failed");
assert!(row.is_some(), "PLC token should be created in database");
let row = row.unwrap();
assert!(row.token.len() == 11, "Token should be in format xxxxx-xxxxx");
assert!(row.token.contains('-'), "Token should contain hyphen");
assert!(row.expires_at > chrono::Utc::now(), "Token should not be expired");
}
#[tokio::test]
async fn test_request_plc_operation_replaces_existing_token() {
let client = client();
let (token, did) = create_account_and_login(&client).await;
let res1 = client
.post(format!(
"{}/xrpc/com.atproto.identity.requestPlcOperationSignature",
base_url().await
))
.bearer_auth(&token)
.send()
.await
.expect("Request 1 failed");
assert_eq!(res1.status(), StatusCode::OK);
let db_url = get_db_connection_string().await;
let pool = PgPool::connect(&db_url).await.expect("DB connect failed");
let token1 = sqlx::query_scalar!(
r#"
SELECT t.token
FROM plc_operation_tokens t
JOIN users u ON t.user_id = u.id
WHERE u.did = $1
"#,
did
)
.fetch_one(&pool)
.await
.expect("Query failed");
let res2 = client
.post(format!(
"{}/xrpc/com.atproto.identity.requestPlcOperationSignature",
base_url().await
))
.bearer_auth(&token)
.send()
.await
.expect("Request 2 failed");
assert_eq!(res2.status(), StatusCode::OK);
let token2 = sqlx::query_scalar!(
r#"
SELECT t.token
FROM plc_operation_tokens t
JOIN users u ON t.user_id = u.id
WHERE u.did = $1
"#,
did
)
.fetch_one(&pool)
.await
.expect("Query failed");
assert_ne!(token1, token2, "Second request should generate a new token");
let count: i64 = sqlx::query_scalar!(
r#"
SELECT COUNT(*) as "count!"
FROM plc_operation_tokens t
JOIN users u ON t.user_id = u.id
WHERE u.did = $1
"#,
did
)
.fetch_one(&pool)
.await
.expect("Count query failed");
assert_eq!(count, 1, "Should only have one token per user");
}
#[tokio::test]
async fn test_submit_plc_operation_wrong_verification_method() {
let client = client();
let (token, did) = create_account_and_login(&client).await;
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| {
format!("127.0.0.1:{}", app_port())
});
let handle = did.split(':').last().unwrap_or("user");
let res = client
.post(format!(
"{}/xrpc/com.atproto.identity.submitPlcOperation",
base_url().await
))
.bearer_auth(&token)
.json(&json!({
"operation": {
"type": "plc_operation",
"rotationKeys": ["did:key:zWrongRotationKey123"],
"verificationMethods": {"atproto": "did:key:zWrongVerificationKey456"},
"alsoKnownAs": [format!("at://{}", handle)],
"services": {
"atproto_pds": {
"type": "AtprotoPersonalDataServer",
"endpoint": format!("https://{}", hostname)
}
},
"prev": null,
"sig": "fake_signature"
}
}))
.send()
.await
.expect("Request failed");
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let body: serde_json::Value = res.json().await.unwrap();
assert_eq!(body["error"], "InvalidRequest");
assert!(
body["message"].as_str().unwrap_or("").contains("signing key") ||
body["message"].as_str().unwrap_or("").contains("rotation"),
"Error should mention key mismatch: {:?}",
body
);
}
#[tokio::test]
async fn test_submit_plc_operation_wrong_handle() {
let client = client();
let (token, _did) = create_account_and_login(&client).await;
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| {
format!("127.0.0.1:{}", app_port())
});
let res = client
.post(format!(
"{}/xrpc/com.atproto.identity.submitPlcOperation",
base_url().await
))
.bearer_auth(&token)
.json(&json!({
"operation": {
"type": "plc_operation",
"rotationKeys": ["did:key:z123"],
"verificationMethods": {"atproto": "did:key:z456"},
"alsoKnownAs": ["at://totally.wrong.handle"],
"services": {
"atproto_pds": {
"type": "AtprotoPersonalDataServer",
"endpoint": format!("https://{}", hostname)
}
},
"prev": null,
"sig": "fake_signature"
}
}))
.send()
.await
.expect("Request failed");
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let body: serde_json::Value = res.json().await.unwrap();
assert_eq!(body["error"], "InvalidRequest");
}
#[tokio::test]
async fn test_submit_plc_operation_wrong_service_type() {
let client = client();
let (token, _did) = create_account_and_login(&client).await;
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| {
format!("127.0.0.1:{}", app_port())
});
let res = client
.post(format!(
"{}/xrpc/com.atproto.identity.submitPlcOperation",
base_url().await
))
.bearer_auth(&token)
.json(&json!({
"operation": {
"type": "plc_operation",
"rotationKeys": ["did:key:z123"],
"verificationMethods": {"atproto": "did:key:z456"},
"alsoKnownAs": ["at://user"],
"services": {
"atproto_pds": {
"type": "WrongServiceType",
"endpoint": format!("https://{}", hostname)
}
},
"prev": null,
"sig": "fake_signature"
}
}))
.send()
.await
.expect("Request failed");
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let body: serde_json::Value = res.json().await.unwrap();
assert_eq!(body["error"], "InvalidRequest");
}
#[tokio::test]
async fn test_plc_token_expiry_format() {
let client = client();
let (token, did) = create_account_and_login(&client).await;
let res = client
.post(format!(
"{}/xrpc/com.atproto.identity.requestPlcOperationSignature",
base_url().await
))
.bearer_auth(&token)
.send()
.await
.expect("Request failed");
assert_eq!(res.status(), StatusCode::OK);
let db_url = get_db_connection_string().await;
let pool = PgPool::connect(&db_url).await.expect("DB connect failed");
let row = sqlx::query!(
r#"
SELECT t.expires_at
FROM plc_operation_tokens t
JOIN users u ON t.user_id = u.id
WHERE u.did = $1
"#,
did
)
.fetch_one(&pool)
.await
.expect("Query failed");
let now = chrono::Utc::now();
let expires = row.expires_at;
let diff = expires - now;
assert!(diff.num_minutes() >= 9, "Token should expire in ~10 minutes, got {} minutes", diff.num_minutes());
assert!(diff.num_minutes() <= 11, "Token should expire in ~10 minutes, got {} minutes", diff.num_minutes());
}