this repo has no description
1mod common;
2use common::*;
3use reqwest::StatusCode;
4use serde_json::{Value, json};
5use wiremock::matchers::{method, path};
6use wiremock::{Mock, MockServer, ResponseTemplate};
7
8#[tokio::test]
9async fn test_create_self_hosted_did_web() {
10 let client = client();
11 let handle = format!("selfweb_{}", uuid::Uuid::new_v4());
12 let payload = json!({
13 "handle": handle,
14 "email": format!("{}@example.com", handle),
15 "password": "Testpass123!",
16 "didType": "web"
17 });
18 let res = client
19 .post(format!(
20 "{}/xrpc/com.atproto.server.createAccount",
21 base_url().await
22 ))
23 .json(&payload)
24 .send()
25 .await
26 .expect("Failed to send request");
27 if res.status() != StatusCode::OK {
28 let body: Value = res.json().await.unwrap_or(json!({"error": "parse failed"}));
29 panic!("createAccount failed: {:?}", body);
30 }
31 let body: Value = res.json().await.expect("Response was not JSON");
32 let did = body["did"].as_str().expect("No DID in response");
33 assert!(
34 did.starts_with("did:web:"),
35 "DID should start with did:web:, got: {}",
36 did
37 );
38 assert!(
39 did.contains(&handle),
40 "DID should contain handle {}, got: {}",
41 handle,
42 did
43 );
44 assert!(
45 !did.contains(":u:"),
46 "Self-hosted did:web should use subdomain format (no :u:), got: {}",
47 did
48 );
49 let jwt = verify_new_account(&client, did).await;
50 let res = client
51 .get(format!("{}/u/{}/did.json", base_url().await, handle))
52 .send()
53 .await
54 .expect("Failed to fetch DID doc via path");
55 assert_eq!(
56 res.status(),
57 StatusCode::OK,
58 "Self-hosted did:web should have DID doc served by PDS (via path for backwards compat)"
59 );
60 let doc: Value = res.json().await.expect("DID doc was not JSON");
61 assert_eq!(doc["id"], did);
62 assert!(
63 doc["verificationMethod"][0]["publicKeyMultibase"].is_string(),
64 "DID doc should have publicKeyMultibase"
65 );
66 let res = client
67 .post(format!(
68 "{}/xrpc/com.atproto.repo.createRecord",
69 base_url().await
70 ))
71 .bearer_auth(&jwt)
72 .json(&json!({
73 "repo": did,
74 "collection": "app.bsky.feed.post",
75 "record": {
76 "$type": "app.bsky.feed.post",
77 "text": "Hello from did:web!",
78 "createdAt": chrono::Utc::now().to_rfc3339()
79 }
80 }))
81 .send()
82 .await
83 .expect("Failed to create post");
84 assert_eq!(
85 res.status(),
86 StatusCode::OK,
87 "Self-hosted did:web account should be able to create records"
88 );
89}
90
91#[tokio::test]
92async fn test_external_did_web_no_local_doc() {
93 let client = client();
94 let mock_server = MockServer::start().await;
95 let mock_uri = mock_server.uri();
96 let mock_addr = mock_uri.trim_start_matches("http://");
97 let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
98 let handle = format!("extweb_{}", uuid::Uuid::new_v4());
99 let pds_endpoint = base_url().await.replace("http://", "https://");
100
101 let reserve_res = client
102 .post(format!(
103 "{}/xrpc/com.atproto.server.reserveSigningKey",
104 base_url().await
105 ))
106 .json(&json!({ "did": did }))
107 .send()
108 .await
109 .expect("Failed to reserve signing key");
110 assert_eq!(reserve_res.status(), StatusCode::OK);
111 let reserve_body: Value = reserve_res.json().await.expect("Response was not JSON");
112 let signing_key = reserve_body["signingKey"]
113 .as_str()
114 .expect("No signingKey returned");
115 let public_key_multibase = signing_key
116 .strip_prefix("did:key:")
117 .expect("signingKey should start with did:key:");
118
119 let did_doc = json!({
120 "@context": ["https://www.w3.org/ns/did/v1"],
121 "id": did,
122 "verificationMethod": [{
123 "id": format!("{}#atproto", did),
124 "type": "Multikey",
125 "controller": did,
126 "publicKeyMultibase": public_key_multibase
127 }],
128 "service": [{
129 "id": "#atproto_pds",
130 "type": "AtprotoPersonalDataServer",
131 "serviceEndpoint": pds_endpoint
132 }]
133 });
134 Mock::given(method("GET"))
135 .and(path("/.well-known/did.json"))
136 .respond_with(ResponseTemplate::new(200).set_body_json(did_doc))
137 .mount(&mock_server)
138 .await;
139 let payload = json!({
140 "handle": handle,
141 "email": format!("{}@example.com", handle),
142 "password": "Testpass123!",
143 "didType": "web-external",
144 "did": did,
145 "signingKey": signing_key
146 });
147 let res = client
148 .post(format!(
149 "{}/xrpc/com.atproto.server.createAccount",
150 base_url().await
151 ))
152 .json(&payload)
153 .send()
154 .await
155 .expect("Failed to send request");
156 if res.status() != StatusCode::OK {
157 let body: Value = res.json().await.unwrap_or(json!({"error": "parse failed"}));
158 panic!("createAccount failed: {:?}", body);
159 }
160 let res = client
161 .get(format!("{}/u/{}/did.json", base_url().await, handle))
162 .send()
163 .await
164 .expect("Failed to fetch DID doc");
165 assert_eq!(
166 res.status(),
167 StatusCode::NOT_FOUND,
168 "External did:web should NOT have DID doc served by PDS"
169 );
170 let body: Value = res.json().await.expect("Response was not JSON");
171 assert!(
172 body["message"].as_str().unwrap_or("").contains("External"),
173 "Error message should indicate external did:web"
174 );
175}
176
177#[tokio::test]
178async fn test_plc_operations_blocked_for_did_web() {
179 let client = client();
180 let handle = format!("plcblock_{}", uuid::Uuid::new_v4());
181 let payload = json!({
182 "handle": handle,
183 "email": format!("{}@example.com", handle),
184 "password": "Testpass123!",
185 "didType": "web"
186 });
187 let res = client
188 .post(format!(
189 "{}/xrpc/com.atproto.server.createAccount",
190 base_url().await
191 ))
192 .json(&payload)
193 .send()
194 .await
195 .expect("Failed to send request");
196 assert_eq!(res.status(), StatusCode::OK);
197 let body: Value = res.json().await.expect("Response was not JSON");
198 let did = body["did"].as_str().expect("No DID").to_string();
199 let jwt = verify_new_account(&client, &did).await;
200 let res = client
201 .post(format!(
202 "{}/xrpc/com.atproto.identity.signPlcOperation",
203 base_url().await
204 ))
205 .bearer_auth(&jwt)
206 .json(&json!({
207 "token": "fake-token"
208 }))
209 .send()
210 .await
211 .expect("Failed to send request");
212 assert_eq!(
213 res.status(),
214 StatusCode::BAD_REQUEST,
215 "signPlcOperation should be blocked for did:web users"
216 );
217 let body: Value = res.json().await.expect("Response was not JSON");
218 assert!(
219 body["message"].as_str().unwrap_or("").contains("did:plc"),
220 "Error should mention did:plc: {:?}",
221 body
222 );
223 let res = client
224 .post(format!(
225 "{}/xrpc/com.atproto.identity.submitPlcOperation",
226 base_url().await
227 ))
228 .bearer_auth(&jwt)
229 .json(&json!({
230 "operation": {}
231 }))
232 .send()
233 .await
234 .expect("Failed to send request");
235 assert_eq!(
236 res.status(),
237 StatusCode::BAD_REQUEST,
238 "submitPlcOperation should be blocked for did:web users"
239 );
240}
241
242#[tokio::test]
243async fn test_get_recommended_did_credentials_no_rotation_keys_for_did_web() {
244 let client = client();
245 let handle = format!("creds_{}", uuid::Uuid::new_v4());
246 let payload = json!({
247 "handle": handle,
248 "email": format!("{}@example.com", handle),
249 "password": "Testpass123!",
250 "didType": "web"
251 });
252 let res = client
253 .post(format!(
254 "{}/xrpc/com.atproto.server.createAccount",
255 base_url().await
256 ))
257 .json(&payload)
258 .send()
259 .await
260 .expect("Failed to send request");
261 assert_eq!(res.status(), StatusCode::OK);
262 let body: Value = res.json().await.expect("Response was not JSON");
263 let did = body["did"].as_str().expect("No DID").to_string();
264 let jwt = verify_new_account(&client, &did).await;
265 let res = client
266 .get(format!(
267 "{}/xrpc/com.atproto.identity.getRecommendedDidCredentials",
268 base_url().await
269 ))
270 .bearer_auth(&jwt)
271 .send()
272 .await
273 .expect("Failed to send request");
274 assert_eq!(res.status(), StatusCode::OK);
275 let body: Value = res.json().await.expect("Response was not JSON");
276 let rotation_keys = body["rotationKeys"]
277 .as_array()
278 .expect("rotationKeys should be an array");
279 assert!(
280 rotation_keys.is_empty(),
281 "did:web should have no rotation keys, got: {:?}",
282 rotation_keys
283 );
284 assert!(
285 body["verificationMethods"].is_object(),
286 "verificationMethods should be present"
287 );
288 assert!(body["services"].is_object(), "services should be present");
289}
290
291#[tokio::test]
292async fn test_did_plc_still_works_with_did_type_param() {
293 let client = client();
294 let handle = format!("plctype_{}", uuid::Uuid::new_v4());
295 let payload = json!({
296 "handle": handle,
297 "email": format!("{}@example.com", handle),
298 "password": "Testpass123!",
299 "didType": "plc"
300 });
301 let res = client
302 .post(format!(
303 "{}/xrpc/com.atproto.server.createAccount",
304 base_url().await
305 ))
306 .json(&payload)
307 .send()
308 .await
309 .expect("Failed to send request");
310 assert_eq!(res.status(), StatusCode::OK);
311 let body: Value = res.json().await.expect("Response was not JSON");
312 let did = body["did"].as_str().expect("No DID").to_string();
313 assert!(
314 did.starts_with("did:plc:"),
315 "DID with didType=plc should be did:plc:, got: {}",
316 did
317 );
318}
319
320#[tokio::test]
321async fn test_external_did_web_requires_did_field() {
322 let client = client();
323 let handle = format!("nodid_{}", uuid::Uuid::new_v4());
324 let payload = json!({
325 "handle": handle,
326 "email": format!("{}@example.com", handle),
327 "password": "Testpass123!",
328 "didType": "web-external"
329 });
330 let res = client
331 .post(format!(
332 "{}/xrpc/com.atproto.server.createAccount",
333 base_url().await
334 ))
335 .json(&payload)
336 .send()
337 .await
338 .expect("Failed to send request");
339 assert_eq!(
340 res.status(),
341 StatusCode::BAD_REQUEST,
342 "web-external without did should fail"
343 );
344 let body: Value = res.json().await.expect("Response was not JSON");
345 assert!(
346 body["message"].as_str().unwrap_or("").contains("did"),
347 "Error should mention did field is required: {:?}",
348 body
349 );
350}