mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-30 05:55:33 +00:00
351 lines
11 KiB
Rust
351 lines
11 KiB
Rust
mod common;
|
|
use common::*;
|
|
use reqwest::StatusCode;
|
|
use serde_json::{Value, json};
|
|
use wiremock::matchers::{method, path};
|
|
use wiremock::{Mock, MockServer, ResponseTemplate};
|
|
|
|
#[tokio::test]
|
|
async fn test_create_self_hosted_did_web() {
|
|
let client = client();
|
|
let handle = format!("selfweb_{}", uuid::Uuid::new_v4());
|
|
let payload = json!({
|
|
"handle": handle,
|
|
"email": format!("{}@example.com", handle),
|
|
"password": "Testpass123!",
|
|
"didType": "web"
|
|
});
|
|
let res = client
|
|
.post(format!(
|
|
"{}/xrpc/com.atproto.server.createAccount",
|
|
base_url().await
|
|
))
|
|
.json(&payload)
|
|
.send()
|
|
.await
|
|
.expect("Failed to send request");
|
|
if res.status() != StatusCode::OK {
|
|
let body: Value = res.json().await.unwrap_or(json!({"error": "parse failed"}));
|
|
panic!("createAccount failed: {:?}", body);
|
|
}
|
|
let body: Value = res.json().await.expect("Response was not JSON");
|
|
let did = body["did"].as_str().expect("No DID in response");
|
|
assert!(
|
|
did.starts_with("did:web:"),
|
|
"DID should start with did:web:, got: {}",
|
|
did
|
|
);
|
|
assert!(
|
|
did.contains(&handle),
|
|
"DID should contain handle {}, got: {}",
|
|
handle,
|
|
did
|
|
);
|
|
assert!(
|
|
!did.contains(":u:"),
|
|
"Self-hosted did:web should use subdomain format (no :u:), got: {}",
|
|
did
|
|
);
|
|
let jwt = verify_new_account(&client, did).await;
|
|
let res = client
|
|
.get(format!("{}/u/{}/did.json", base_url().await, handle))
|
|
.send()
|
|
.await
|
|
.expect("Failed to fetch DID doc via path");
|
|
assert_eq!(
|
|
res.status(),
|
|
StatusCode::OK,
|
|
"Self-hosted did:web should have DID doc served by PDS (via path for backwards compat)"
|
|
);
|
|
let doc: Value = res.json().await.expect("DID doc was not JSON");
|
|
assert_eq!(doc["id"], did);
|
|
assert!(
|
|
doc["verificationMethod"][0]["publicKeyMultibase"].is_string(),
|
|
"DID doc should have publicKeyMultibase"
|
|
);
|
|
let res = client
|
|
.post(format!(
|
|
"{}/xrpc/com.atproto.repo.createRecord",
|
|
base_url().await
|
|
))
|
|
.bearer_auth(&jwt)
|
|
.json(&json!({
|
|
"repo": did,
|
|
"collection": "app.bsky.feed.post",
|
|
"record": {
|
|
"$type": "app.bsky.feed.post",
|
|
"text": "Hello from did:web!",
|
|
"createdAt": chrono::Utc::now().to_rfc3339()
|
|
}
|
|
}))
|
|
.send()
|
|
.await
|
|
.expect("Failed to create post");
|
|
assert_eq!(
|
|
res.status(),
|
|
StatusCode::OK,
|
|
"Self-hosted did:web account should be able to create records"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_external_did_web_no_local_doc() {
|
|
let client = client();
|
|
let mock_server = MockServer::start().await;
|
|
let mock_uri = mock_server.uri();
|
|
let mock_addr = mock_uri.trim_start_matches("http://");
|
|
let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
|
|
let handle = format!("extweb_{}", uuid::Uuid::new_v4());
|
|
let pds_endpoint = base_url().await.replace("http://", "https://");
|
|
|
|
let reserve_res = client
|
|
.post(format!(
|
|
"{}/xrpc/com.atproto.server.reserveSigningKey",
|
|
base_url().await
|
|
))
|
|
.json(&json!({ "did": did }))
|
|
.send()
|
|
.await
|
|
.expect("Failed to reserve signing key");
|
|
assert_eq!(reserve_res.status(), StatusCode::OK);
|
|
let reserve_body: Value = reserve_res.json().await.expect("Response was not JSON");
|
|
let signing_key = reserve_body["signingKey"]
|
|
.as_str()
|
|
.expect("No signingKey returned");
|
|
let public_key_multibase = signing_key
|
|
.strip_prefix("did:key:")
|
|
.expect("signingKey should start with did:key:");
|
|
|
|
let did_doc = json!({
|
|
"@context": ["https://www.w3.org/ns/did/v1"],
|
|
"id": did,
|
|
"verificationMethod": [{
|
|
"id": format!("{}#atproto", did),
|
|
"type": "Multikey",
|
|
"controller": did,
|
|
"publicKeyMultibase": public_key_multibase
|
|
}],
|
|
"service": [{
|
|
"id": "#atproto_pds",
|
|
"type": "AtprotoPersonalDataServer",
|
|
"serviceEndpoint": pds_endpoint
|
|
}]
|
|
});
|
|
Mock::given(method("GET"))
|
|
.and(path("/.well-known/did.json"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(did_doc))
|
|
.mount(&mock_server)
|
|
.await;
|
|
let payload = json!({
|
|
"handle": handle,
|
|
"email": format!("{}@example.com", handle),
|
|
"password": "Testpass123!",
|
|
"didType": "web-external",
|
|
"did": did,
|
|
"signingKey": signing_key
|
|
});
|
|
let res = client
|
|
.post(format!(
|
|
"{}/xrpc/com.atproto.server.createAccount",
|
|
base_url().await
|
|
))
|
|
.json(&payload)
|
|
.send()
|
|
.await
|
|
.expect("Failed to send request");
|
|
if res.status() != StatusCode::OK {
|
|
let body: Value = res.json().await.unwrap_or(json!({"error": "parse failed"}));
|
|
panic!("createAccount failed: {:?}", body);
|
|
}
|
|
let res = client
|
|
.get(format!("{}/u/{}/did.json", base_url().await, handle))
|
|
.send()
|
|
.await
|
|
.expect("Failed to fetch DID doc");
|
|
assert_eq!(
|
|
res.status(),
|
|
StatusCode::NOT_FOUND,
|
|
"External did:web should NOT have DID doc served by PDS"
|
|
);
|
|
let body: Value = res.json().await.expect("Response was not JSON");
|
|
assert!(
|
|
body["message"].as_str().unwrap_or("").contains("External"),
|
|
"Error message should indicate external did:web"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_plc_operations_blocked_for_did_web() {
|
|
let client = client();
|
|
let handle = format!("plcblock_{}", uuid::Uuid::new_v4());
|
|
let payload = json!({
|
|
"handle": handle,
|
|
"email": format!("{}@example.com", handle),
|
|
"password": "Testpass123!",
|
|
"didType": "web"
|
|
});
|
|
let res = client
|
|
.post(format!(
|
|
"{}/xrpc/com.atproto.server.createAccount",
|
|
base_url().await
|
|
))
|
|
.json(&payload)
|
|
.send()
|
|
.await
|
|
.expect("Failed to send request");
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
let body: Value = res.json().await.expect("Response was not JSON");
|
|
let did = body["did"].as_str().expect("No DID").to_string();
|
|
let jwt = verify_new_account(&client, &did).await;
|
|
let res = client
|
|
.post(format!(
|
|
"{}/xrpc/com.atproto.identity.signPlcOperation",
|
|
base_url().await
|
|
))
|
|
.bearer_auth(&jwt)
|
|
.json(&json!({
|
|
"token": "fake-token"
|
|
}))
|
|
.send()
|
|
.await
|
|
.expect("Failed to send request");
|
|
assert_eq!(
|
|
res.status(),
|
|
StatusCode::BAD_REQUEST,
|
|
"signPlcOperation should be blocked for did:web users"
|
|
);
|
|
let body: Value = res.json().await.expect("Response was not JSON");
|
|
assert!(
|
|
body["message"].as_str().unwrap_or("").contains("did:plc"),
|
|
"Error should mention did:plc: {:?}",
|
|
body
|
|
);
|
|
let res = client
|
|
.post(format!(
|
|
"{}/xrpc/com.atproto.identity.submitPlcOperation",
|
|
base_url().await
|
|
))
|
|
.bearer_auth(&jwt)
|
|
.json(&json!({
|
|
"operation": {}
|
|
}))
|
|
.send()
|
|
.await
|
|
.expect("Failed to send request");
|
|
assert_eq!(
|
|
res.status(),
|
|
StatusCode::BAD_REQUEST,
|
|
"submitPlcOperation should be blocked for did:web users"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_recommended_did_credentials_no_rotation_keys_for_did_web() {
|
|
let client = client();
|
|
let handle = format!("creds_{}", uuid::Uuid::new_v4());
|
|
let payload = json!({
|
|
"handle": handle,
|
|
"email": format!("{}@example.com", handle),
|
|
"password": "Testpass123!",
|
|
"didType": "web"
|
|
});
|
|
let res = client
|
|
.post(format!(
|
|
"{}/xrpc/com.atproto.server.createAccount",
|
|
base_url().await
|
|
))
|
|
.json(&payload)
|
|
.send()
|
|
.await
|
|
.expect("Failed to send request");
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
let body: Value = res.json().await.expect("Response was not JSON");
|
|
let did = body["did"].as_str().expect("No DID").to_string();
|
|
let jwt = verify_new_account(&client, &did).await;
|
|
let res = client
|
|
.get(format!(
|
|
"{}/xrpc/com.atproto.identity.getRecommendedDidCredentials",
|
|
base_url().await
|
|
))
|
|
.bearer_auth(&jwt)
|
|
.send()
|
|
.await
|
|
.expect("Failed to send request");
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
let body: Value = res.json().await.expect("Response was not JSON");
|
|
let rotation_keys = body["rotationKeys"]
|
|
.as_array()
|
|
.expect("rotationKeys should be an array");
|
|
assert!(
|
|
rotation_keys.is_empty(),
|
|
"did:web should have no rotation keys, got: {:?}",
|
|
rotation_keys
|
|
);
|
|
assert!(
|
|
body["verificationMethods"].is_object(),
|
|
"verificationMethods should be present"
|
|
);
|
|
assert!(body["services"].is_object(), "services should be present");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_did_plc_still_works_with_did_type_param() {
|
|
let client = client();
|
|
let handle = format!("plctype_{}", uuid::Uuid::new_v4());
|
|
let payload = json!({
|
|
"handle": handle,
|
|
"email": format!("{}@example.com", handle),
|
|
"password": "Testpass123!",
|
|
"didType": "plc"
|
|
});
|
|
let res = client
|
|
.post(format!(
|
|
"{}/xrpc/com.atproto.server.createAccount",
|
|
base_url().await
|
|
))
|
|
.json(&payload)
|
|
.send()
|
|
.await
|
|
.expect("Failed to send request");
|
|
assert_eq!(res.status(), StatusCode::OK);
|
|
let body: Value = res.json().await.expect("Response was not JSON");
|
|
let did = body["did"].as_str().expect("No DID").to_string();
|
|
assert!(
|
|
did.starts_with("did:plc:"),
|
|
"DID with didType=plc should be did:plc:, got: {}",
|
|
did
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_external_did_web_requires_did_field() {
|
|
let client = client();
|
|
let handle = format!("nodid_{}", uuid::Uuid::new_v4());
|
|
let payload = json!({
|
|
"handle": handle,
|
|
"email": format!("{}@example.com", handle),
|
|
"password": "Testpass123!",
|
|
"didType": "web-external"
|
|
});
|
|
let res = client
|
|
.post(format!(
|
|
"{}/xrpc/com.atproto.server.createAccount",
|
|
base_url().await
|
|
))
|
|
.json(&payload)
|
|
.send()
|
|
.await
|
|
.expect("Failed to send request");
|
|
assert_eq!(
|
|
res.status(),
|
|
StatusCode::BAD_REQUEST,
|
|
"web-external without did should fail"
|
|
);
|
|
let body: Value = res.json().await.expect("Response was not JSON");
|
|
assert!(
|
|
body["message"].as_str().unwrap_or("").contains("did"),
|
|
"Error should mention did field is required: {:?}",
|
|
body
|
|
);
|
|
}
|