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 ); }