this repo has no description
1mod common;
2use base64::Engine;
3use base64::engine::general_purpose::URL_SAFE_NO_PAD;
4use common::*;
5use k256::ecdsa::{SigningKey, signature::Signer};
6use reqwest::StatusCode;
7use serde_json::{Value, json};
8use wiremock::matchers::{method, path};
9use wiremock::{Mock, MockServer, ResponseTemplate};
10
11#[tokio::test]
12async fn test_create_self_hosted_did_web() {
13 let client = client();
14 let handle = format!("sw{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
15 let payload = json!({
16 "handle": handle,
17 "email": format!("{}@example.com", handle),
18 "password": "Testpass123!",
19 "didType": "web"
20 });
21 let res = client
22 .post(format!(
23 "{}/xrpc/com.atproto.server.createAccount",
24 base_url().await
25 ))
26 .json(&payload)
27 .send()
28 .await
29 .expect("Failed to send request");
30 if res.status() != StatusCode::OK {
31 let body: Value = res.json().await.unwrap_or(json!({"error": "parse failed"}));
32 panic!("createAccount failed: {:?}", body);
33 }
34 let body: Value = res.json().await.expect("Response was not JSON");
35 let did = body["did"].as_str().expect("No DID in response");
36 assert!(
37 did.starts_with("did:web:"),
38 "DID should start with did:web:, got: {}",
39 did
40 );
41 assert!(
42 did.contains(&handle),
43 "DID should contain handle {}, got: {}",
44 handle,
45 did
46 );
47 assert!(
48 !did.contains(":u:"),
49 "Self-hosted did:web should use subdomain format (no :u:), got: {}",
50 did
51 );
52 let jwt = verify_new_account(&client, did).await;
53 let res = client
54 .get(format!("{}/u/{}/did.json", base_url().await, handle))
55 .send()
56 .await
57 .expect("Failed to fetch DID doc via path");
58 assert_eq!(
59 res.status(),
60 StatusCode::OK,
61 "Self-hosted did:web should have DID doc served by PDS (via path for backwards compat)"
62 );
63 let doc: Value = res.json().await.expect("DID doc was not JSON");
64 assert_eq!(doc["id"], did);
65 assert!(
66 doc["verificationMethod"][0]["publicKeyMultibase"].is_string(),
67 "DID doc should have publicKeyMultibase"
68 );
69 let res = client
70 .post(format!(
71 "{}/xrpc/com.atproto.repo.createRecord",
72 base_url().await
73 ))
74 .bearer_auth(&jwt)
75 .json(&json!({
76 "repo": did,
77 "collection": "app.bsky.feed.post",
78 "record": {
79 "$type": "app.bsky.feed.post",
80 "text": "Hello from did:web!",
81 "createdAt": chrono::Utc::now().to_rfc3339()
82 }
83 }))
84 .send()
85 .await
86 .expect("Failed to create post");
87 assert_eq!(
88 res.status(),
89 StatusCode::OK,
90 "Self-hosted did:web account should be able to create records"
91 );
92}
93
94#[tokio::test]
95async fn test_external_did_web_no_local_doc() {
96 let client = client();
97 let mock_server = MockServer::start().await;
98 let mock_uri = mock_server.uri();
99 let mock_addr = mock_uri.trim_start_matches("http://");
100 let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
101 let handle = format!("xw{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
102 let pds_endpoint = base_url().await.replace("http://", "https://");
103
104 let reserve_res = client
105 .post(format!(
106 "{}/xrpc/com.atproto.server.reserveSigningKey",
107 base_url().await
108 ))
109 .json(&json!({ "did": did }))
110 .send()
111 .await
112 .expect("Failed to reserve signing key");
113 assert_eq!(reserve_res.status(), StatusCode::OK);
114 let reserve_body: Value = reserve_res.json().await.expect("Response was not JSON");
115 let signing_key = reserve_body["signingKey"]
116 .as_str()
117 .expect("No signingKey returned");
118 let public_key_multibase = signing_key
119 .strip_prefix("did:key:")
120 .expect("signingKey should start with did:key:");
121
122 let did_doc = json!({
123 "@context": ["https://www.w3.org/ns/did/v1"],
124 "id": did,
125 "verificationMethod": [{
126 "id": format!("{}#atproto", did),
127 "type": "Multikey",
128 "controller": did,
129 "publicKeyMultibase": public_key_multibase
130 }],
131 "service": [{
132 "id": "#atproto_pds",
133 "type": "AtprotoPersonalDataServer",
134 "serviceEndpoint": pds_endpoint
135 }]
136 });
137 Mock::given(method("GET"))
138 .and(path("/.well-known/did.json"))
139 .respond_with(ResponseTemplate::new(200).set_body_json(did_doc))
140 .mount(&mock_server)
141 .await;
142 let payload = json!({
143 "handle": handle,
144 "email": format!("{}@example.com", handle),
145 "password": "Testpass123!",
146 "didType": "web-external",
147 "did": did,
148 "signingKey": signing_key
149 });
150 let res = client
151 .post(format!(
152 "{}/xrpc/com.atproto.server.createAccount",
153 base_url().await
154 ))
155 .json(&payload)
156 .send()
157 .await
158 .expect("Failed to send request");
159 if res.status() != StatusCode::OK {
160 let body: Value = res.json().await.unwrap_or(json!({"error": "parse failed"}));
161 panic!("createAccount failed: {:?}", body);
162 }
163 let res = client
164 .get(format!("{}/u/{}/did.json", base_url().await, handle))
165 .send()
166 .await
167 .expect("Failed to fetch DID doc");
168 assert_eq!(
169 res.status(),
170 StatusCode::NOT_FOUND,
171 "External did:web should NOT have DID doc served by PDS"
172 );
173 let body: Value = res.json().await.expect("Response was not JSON");
174 assert!(
175 body["message"].as_str().unwrap_or("").contains("External"),
176 "Error message should indicate external did:web"
177 );
178}
179
180#[tokio::test]
181async fn test_plc_operations_blocked_for_did_web() {
182 let client = client();
183 let handle = format!("pb{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
184 let payload = json!({
185 "handle": handle,
186 "email": format!("{}@example.com", handle),
187 "password": "Testpass123!",
188 "didType": "web"
189 });
190 let res = client
191 .post(format!(
192 "{}/xrpc/com.atproto.server.createAccount",
193 base_url().await
194 ))
195 .json(&payload)
196 .send()
197 .await
198 .expect("Failed to send request");
199 assert_eq!(res.status(), StatusCode::OK);
200 let body: Value = res.json().await.expect("Response was not JSON");
201 let did = body["did"].as_str().expect("No DID").to_string();
202 let jwt = verify_new_account(&client, &did).await;
203 let res = client
204 .post(format!(
205 "{}/xrpc/com.atproto.identity.signPlcOperation",
206 base_url().await
207 ))
208 .bearer_auth(&jwt)
209 .json(&json!({
210 "token": "fake-token"
211 }))
212 .send()
213 .await
214 .expect("Failed to send request");
215 assert_eq!(
216 res.status(),
217 StatusCode::BAD_REQUEST,
218 "signPlcOperation should be blocked for did:web users"
219 );
220 let body: Value = res.json().await.expect("Response was not JSON");
221 assert!(
222 body["message"].as_str().unwrap_or("").contains("did:plc"),
223 "Error should mention did:plc: {:?}",
224 body
225 );
226 let res = client
227 .post(format!(
228 "{}/xrpc/com.atproto.identity.submitPlcOperation",
229 base_url().await
230 ))
231 .bearer_auth(&jwt)
232 .json(&json!({
233 "operation": {}
234 }))
235 .send()
236 .await
237 .expect("Failed to send request");
238 assert_eq!(
239 res.status(),
240 StatusCode::BAD_REQUEST,
241 "submitPlcOperation should be blocked for did:web users"
242 );
243}
244
245#[tokio::test]
246async fn test_get_recommended_did_credentials_no_rotation_keys_for_did_web() {
247 let client = client();
248 let handle = format!("cr{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
249 let payload = json!({
250 "handle": handle,
251 "email": format!("{}@example.com", handle),
252 "password": "Testpass123!",
253 "didType": "web"
254 });
255 let res = client
256 .post(format!(
257 "{}/xrpc/com.atproto.server.createAccount",
258 base_url().await
259 ))
260 .json(&payload)
261 .send()
262 .await
263 .expect("Failed to send request");
264 assert_eq!(res.status(), StatusCode::OK);
265 let body: Value = res.json().await.expect("Response was not JSON");
266 let did = body["did"].as_str().expect("No DID").to_string();
267 let jwt = verify_new_account(&client, &did).await;
268 let res = client
269 .get(format!(
270 "{}/xrpc/com.atproto.identity.getRecommendedDidCredentials",
271 base_url().await
272 ))
273 .bearer_auth(&jwt)
274 .send()
275 .await
276 .expect("Failed to send request");
277 assert_eq!(res.status(), StatusCode::OK);
278 let body: Value = res.json().await.expect("Response was not JSON");
279 let rotation_keys = body["rotationKeys"]
280 .as_array()
281 .expect("rotationKeys should be an array");
282 assert!(
283 rotation_keys.is_empty(),
284 "did:web should have no rotation keys, got: {:?}",
285 rotation_keys
286 );
287 assert!(
288 body["verificationMethods"].is_object(),
289 "verificationMethods should be present"
290 );
291 assert!(body["services"].is_object(), "services should be present");
292}
293
294#[tokio::test]
295async fn test_did_plc_still_works_with_did_type_param() {
296 let client = client();
297 let handle = format!("pt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
298 let payload = json!({
299 "handle": handle,
300 "email": format!("{}@example.com", handle),
301 "password": "Testpass123!",
302 "didType": "plc"
303 });
304 let res = client
305 .post(format!(
306 "{}/xrpc/com.atproto.server.createAccount",
307 base_url().await
308 ))
309 .json(&payload)
310 .send()
311 .await
312 .expect("Failed to send request");
313 assert_eq!(res.status(), StatusCode::OK);
314 let body: Value = res.json().await.expect("Response was not JSON");
315 let did = body["did"].as_str().expect("No DID").to_string();
316 assert!(
317 did.starts_with("did:plc:"),
318 "DID with didType=plc should be did:plc:, got: {}",
319 did
320 );
321}
322
323#[tokio::test]
324async fn test_external_did_web_requires_did_field() {
325 let client = client();
326 let handle = format!("nd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
327 let payload = json!({
328 "handle": handle,
329 "email": format!("{}@example.com", handle),
330 "password": "Testpass123!",
331 "didType": "web-external"
332 });
333 let res = client
334 .post(format!(
335 "{}/xrpc/com.atproto.server.createAccount",
336 base_url().await
337 ))
338 .json(&payload)
339 .send()
340 .await
341 .expect("Failed to send request");
342 assert_eq!(
343 res.status(),
344 StatusCode::BAD_REQUEST,
345 "web-external without did should fail"
346 );
347 let body: Value = res.json().await.expect("Response was not JSON");
348 assert!(
349 body["message"].as_str().unwrap_or("").contains("did"),
350 "Error should mention did field is required: {:?}",
351 body
352 );
353}
354
355fn signing_key_to_multibase(signing_key: &SigningKey) -> String {
356 let verifying_key = signing_key.verifying_key();
357 let compressed = verifying_key.to_sec1_bytes();
358 let mut multicodec = vec![0xe7, 0x01];
359 multicodec.extend_from_slice(&compressed);
360 multibase::encode(multibase::Base::Base58Btc, &multicodec)
361}
362
363fn create_service_jwt(signing_key: &SigningKey, did: &str, aud: &str) -> String {
364 let header = json!({"alg": "ES256K", "typ": "jwt"});
365 let now = chrono::Utc::now().timestamp() as usize;
366 let claims = json!({
367 "iss": did,
368 "sub": did,
369 "aud": aud,
370 "exp": now + 300,
371 "iat": now,
372 "lxm": "com.atproto.server.createAccount",
373 "jti": uuid::Uuid::new_v4().to_string()
374 });
375 let header_b64 = URL_SAFE_NO_PAD.encode(header.to_string());
376 let claims_b64 = URL_SAFE_NO_PAD.encode(claims.to_string());
377 let message = format!("{}.{}", header_b64, claims_b64);
378 let signature: k256::ecdsa::Signature = signing_key.sign(message.as_bytes());
379 let sig_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
380 format!("{}.{}", message, sig_b64)
381}
382
383#[tokio::test]
384async fn test_did_web_byod_flow() {
385 let client = client();
386 let mock_server = MockServer::start().await;
387 let mock_uri = mock_server.uri();
388 let mock_addr = mock_uri.trim_start_matches("http://");
389 let unique_id = uuid::Uuid::new_v4().to_string().replace("-", "");
390 let did = format!(
391 "did:web:{}:byod:{}",
392 mock_addr.replace(":", "%3A"),
393 unique_id
394 );
395 let handle = format!("by{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
396 let pds_endpoint = base_url().await.replace("http://", "https://");
397 let pds_did = format!("did:web:{}", pds_endpoint.trim_start_matches("https://"));
398
399 let temp_key = SigningKey::random(&mut rand::thread_rng());
400 let public_key_multibase = signing_key_to_multibase(&temp_key);
401
402 let did_doc = json!({
403 "@context": ["https://www.w3.org/ns/did/v1"],
404 "id": did,
405 "verificationMethod": [{
406 "id": format!("{}#atproto", did),
407 "type": "Multikey",
408 "controller": did,
409 "publicKeyMultibase": public_key_multibase
410 }],
411 "service": [{
412 "id": "#atproto_pds",
413 "type": "AtprotoPersonalDataServer",
414 "serviceEndpoint": pds_endpoint
415 }]
416 });
417 Mock::given(method("GET"))
418 .and(path(format!("/byod/{}/did.json", unique_id)))
419 .respond_with(ResponseTemplate::new(200).set_body_json(&did_doc))
420 .mount(&mock_server)
421 .await;
422
423 let service_jwt = create_service_jwt(&temp_key, &did, &pds_did);
424 let payload = json!({
425 "handle": handle,
426 "email": format!("{}@example.com", handle),
427 "password": "Testpass123!",
428 "did": did
429 });
430 let res = client
431 .post(format!(
432 "{}/xrpc/com.atproto.server.createAccount",
433 base_url().await
434 ))
435 .header("Authorization", format!("Bearer {}", service_jwt))
436 .json(&payload)
437 .send()
438 .await
439 .expect("Failed to send request");
440 if res.status() != StatusCode::OK {
441 let body: Value = res.json().await.unwrap_or(json!({"error": "parse failed"}));
442 panic!("createAccount BYOD failed: {:?}", body);
443 }
444 let body: Value = res.json().await.expect("Response was not JSON");
445 let returned_did = body["did"].as_str().expect("No DID in response");
446 assert_eq!(returned_did, did, "Returned DID should match requested DID");
447 assert_eq!(
448 body["verificationRequired"], true,
449 "BYOD accounts should require verification"
450 );
451
452 let access_jwt = common::verify_new_account(&client, returned_did).await;
453
454 let res = client
455 .get(format!(
456 "{}/xrpc/com.atproto.server.checkAccountStatus",
457 base_url().await
458 ))
459 .bearer_auth(&access_jwt)
460 .send()
461 .await
462 .expect("Failed to check account status");
463 assert_eq!(res.status(), StatusCode::OK);
464 let status: Value = res.json().await.expect("Response was not JSON");
465 assert_eq!(
466 status["activated"], false,
467 "BYOD account should be deactivated initially"
468 );
469
470 let res = client
471 .get(format!(
472 "{}/xrpc/com.atproto.identity.getRecommendedDidCredentials",
473 base_url().await
474 ))
475 .bearer_auth(&access_jwt)
476 .send()
477 .await
478 .expect("Failed to get recommended credentials");
479 assert_eq!(res.status(), StatusCode::OK);
480 let creds: Value = res.json().await.expect("Response was not JSON");
481 assert!(
482 creds["verificationMethods"]["atproto"].is_string(),
483 "Should return PDS signing key"
484 );
485 let pds_signing_key = creds["verificationMethods"]["atproto"]
486 .as_str()
487 .expect("No atproto verification method");
488 assert!(
489 pds_signing_key.starts_with("did:key:"),
490 "PDS signing key should be did:key format"
491 );
492
493 let res = client
494 .post(format!(
495 "{}/xrpc/com.atproto.server.activateAccount",
496 base_url().await
497 ))
498 .bearer_auth(&access_jwt)
499 .send()
500 .await
501 .expect("Failed to activate account");
502 assert_eq!(
503 res.status(),
504 StatusCode::OK,
505 "activateAccount should succeed"
506 );
507
508 let res = client
509 .get(format!(
510 "{}/xrpc/com.atproto.server.checkAccountStatus",
511 base_url().await
512 ))
513 .bearer_auth(&access_jwt)
514 .send()
515 .await
516 .expect("Failed to check account status");
517 assert_eq!(res.status(), StatusCode::OK);
518 let status: Value = res.json().await.expect("Response was not JSON");
519 assert_eq!(
520 status["activated"], true,
521 "Account should be activated after activateAccount call"
522 );
523
524 let res = client
525 .post(format!(
526 "{}/xrpc/com.atproto.repo.createRecord",
527 base_url().await
528 ))
529 .bearer_auth(&access_jwt)
530 .json(&json!({
531 "repo": did,
532 "collection": "app.bsky.feed.post",
533 "record": {
534 "$type": "app.bsky.feed.post",
535 "text": "Hello from BYOD did:web!",
536 "createdAt": chrono::Utc::now().to_rfc3339()
537 }
538 }))
539 .send()
540 .await
541 .expect("Failed to create post");
542 assert_eq!(
543 res.status(),
544 StatusCode::OK,
545 "Activated BYOD account should be able to create records"
546 );
547}
548
549#[tokio::test]
550async fn test_deactivate_with_migrating_to() {
551 let client = client();
552 let base = base_url().await;
553 let handle = format!("mig{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
554 let payload = json!({
555 "handle": handle,
556 "email": format!("{}@example.com", handle),
557 "password": "Testpass123!",
558 "didType": "web"
559 });
560 let res = client
561 .post(format!("{}/xrpc/com.atproto.server.createAccount", base))
562 .json(&payload)
563 .send()
564 .await
565 .expect("Failed to send request");
566 assert_eq!(res.status(), StatusCode::OK);
567 let body: Value = res.json().await.expect("Response was not JSON");
568 let did = body["did"].as_str().expect("No DID").to_string();
569 let jwt = verify_new_account(&client, &did).await;
570 let target_pds = "https://pds2.example.com";
571 let res = client
572 .post(format!(
573 "{}/xrpc/com.atproto.server.deactivateAccount",
574 base
575 ))
576 .bearer_auth(&jwt)
577 .json(&json!({ "migratingTo": target_pds }))
578 .send()
579 .await
580 .expect("Failed to send request");
581 assert_eq!(res.status(), StatusCode::OK);
582 let pool = get_test_db_pool().await;
583 let row = sqlx::query!(
584 r#"SELECT migrated_to_pds, deactivated_at FROM users WHERE did = $1"#,
585 &did
586 )
587 .fetch_one(pool)
588 .await
589 .expect("Failed to query user");
590 assert_eq!(
591 row.migrated_to_pds.as_deref(),
592 Some(target_pds),
593 "migrated_to_pds should be set to target PDS"
594 );
595 assert!(
596 row.deactivated_at.is_some(),
597 "deactivated_at should be set for migrated account"
598 );
599}
600
601#[tokio::test]
602async fn test_migrated_account_blocked_from_repo_ops() {
603 let client = client();
604 let base = base_url().await;
605 let handle = format!("blk{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
606 let payload = json!({
607 "handle": handle,
608 "email": format!("{}@example.com", handle),
609 "password": "Testpass123!",
610 "didType": "web"
611 });
612 let res = client
613 .post(format!("{}/xrpc/com.atproto.server.createAccount", base))
614 .json(&payload)
615 .send()
616 .await
617 .expect("Failed to send request");
618 assert_eq!(res.status(), StatusCode::OK);
619 let body: Value = res.json().await.expect("Response was not JSON");
620 let did = body["did"].as_str().expect("No DID").to_string();
621 let jwt = verify_new_account(&client, &did).await;
622 let res = client
623 .post(format!("{}/xrpc/com.atproto.repo.createRecord", base))
624 .bearer_auth(&jwt)
625 .json(&json!({
626 "repo": did,
627 "collection": "app.bsky.feed.post",
628 "record": {
629 "$type": "app.bsky.feed.post",
630 "text": "Pre-migration post",
631 "createdAt": chrono::Utc::now().to_rfc3339()
632 }
633 }))
634 .send()
635 .await
636 .expect("Failed to send request");
637 assert_eq!(res.status(), StatusCode::OK);
638 let res = client
639 .post(format!(
640 "{}/xrpc/com.atproto.server.deactivateAccount",
641 base
642 ))
643 .bearer_auth(&jwt)
644 .json(&json!({ "migratingTo": "https://pds2.example.com" }))
645 .send()
646 .await
647 .expect("Failed to send request");
648 assert_eq!(res.status(), StatusCode::OK);
649 let res = client
650 .post(format!("{}/xrpc/com.atproto.repo.createRecord", base))
651 .bearer_auth(&jwt)
652 .json(&json!({
653 "repo": did,
654 "collection": "app.bsky.feed.post",
655 "record": {
656 "$type": "app.bsky.feed.post",
657 "text": "Post-migration post - should fail",
658 "createdAt": chrono::Utc::now().to_rfc3339()
659 }
660 }))
661 .send()
662 .await
663 .expect("Failed to send request");
664 assert!(
665 res.status().is_client_error(),
666 "createRecord should fail for migrated account: {}",
667 res.status()
668 );
669 let res = client
670 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base))
671 .bearer_auth(&jwt)
672 .json(&json!({
673 "repo": did,
674 "collection": "app.bsky.actor.profile",
675 "rkey": "self",
676 "record": {
677 "$type": "app.bsky.actor.profile",
678 "displayName": "Test"
679 }
680 }))
681 .send()
682 .await
683 .expect("Failed to send request");
684 assert!(
685 res.status().is_client_error(),
686 "putRecord should fail for migrated account: {}",
687 res.status()
688 );
689 let res = client
690 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base))
691 .bearer_auth(&jwt)
692 .json(&json!({
693 "repo": did,
694 "collection": "app.bsky.feed.post",
695 "rkey": "test123"
696 }))
697 .send()
698 .await
699 .expect("Failed to send request");
700 assert!(
701 res.status().is_client_error(),
702 "deleteRecord should fail for migrated account: {}",
703 res.status()
704 );
705 let res = client
706 .post(format!("{}/xrpc/com.atproto.repo.applyWrites", base))
707 .bearer_auth(&jwt)
708 .json(&json!({
709 "repo": did,
710 "writes": [{
711 "$type": "com.atproto.repo.applyWrites#create",
712 "collection": "app.bsky.feed.post",
713 "value": {
714 "$type": "app.bsky.feed.post",
715 "text": "Batch post",
716 "createdAt": chrono::Utc::now().to_rfc3339()
717 }
718 }]
719 }))
720 .send()
721 .await
722 .expect("Failed to send request");
723 assert!(
724 res.status().is_client_error(),
725 "applyWrites should fail for migrated account: {}",
726 res.status()
727 );
728 let res = client
729 .post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base))
730 .bearer_auth(&jwt)
731 .header("Content-Type", "text/plain")
732 .body("test blob content")
733 .send()
734 .await
735 .expect("Failed to send request");
736 assert!(
737 res.status().is_client_error(),
738 "uploadBlob should fail for migrated account: {}",
739 res.status()
740 );
741}
742
743#[tokio::test]
744async fn test_migrated_session_status() {
745 let client = client();
746 let base = base_url().await;
747 let handle = format!("ses{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
748 let payload = json!({
749 "handle": handle,
750 "email": format!("{}@example.com", handle),
751 "password": "Testpass123!",
752 "didType": "web"
753 });
754 let res = client
755 .post(format!("{}/xrpc/com.atproto.server.createAccount", base))
756 .json(&payload)
757 .send()
758 .await
759 .expect("Failed to send request");
760 assert_eq!(res.status(), StatusCode::OK);
761 let body: Value = res.json().await.expect("Response was not JSON");
762 let did = body["did"].as_str().expect("No DID").to_string();
763 let jwt = verify_new_account(&client, &did).await;
764 let res = client
765 .get(format!("{}/xrpc/com.atproto.server.getSession", base))
766 .bearer_auth(&jwt)
767 .send()
768 .await
769 .expect("Failed to send request");
770 assert_eq!(res.status(), StatusCode::OK);
771 let body: Value = res.json().await.expect("Response was not JSON");
772 assert_eq!(body["active"], true);
773 assert!(
774 body["status"].is_null() || body["status"] == "active",
775 "Status should be null or 'active' for normal accounts"
776 );
777 let target_pds = "https://pds3.example.com";
778 let res = client
779 .post(format!(
780 "{}/xrpc/com.atproto.server.deactivateAccount",
781 base
782 ))
783 .bearer_auth(&jwt)
784 .json(&json!({ "migratingTo": target_pds }))
785 .send()
786 .await
787 .expect("Failed to send request");
788 assert_eq!(res.status(), StatusCode::OK);
789 let res = client
790 .get(format!("{}/xrpc/com.atproto.server.getSession", base))
791 .bearer_auth(&jwt)
792 .send()
793 .await
794 .expect("Failed to send request");
795 assert_eq!(res.status(), StatusCode::OK);
796 let body: Value = res.json().await.expect("Response was not JSON");
797 assert_eq!(
798 body["active"], false,
799 "Migrated account should not be active"
800 );
801 assert_eq!(
802 body["status"], "migrated",
803 "Status should be 'migrated' after migration"
804 );
805 assert_eq!(
806 body["migratedToPds"], target_pds,
807 "migratedToPds should be set to target PDS"
808 );
809}
810
811#[tokio::test]
812async fn test_migrating_to_ignored_for_did_plc() {
813 let client = client();
814 let base = base_url().await;
815 let handle = format!("plc{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
816 let payload = json!({
817 "handle": handle,
818 "email": format!("{}@example.com", handle),
819 "password": "Testpass123!",
820 "didType": "plc"
821 });
822 let res = client
823 .post(format!("{}/xrpc/com.atproto.server.createAccount", base))
824 .json(&payload)
825 .send()
826 .await
827 .expect("Failed to send request");
828 assert_eq!(res.status(), StatusCode::OK);
829 let body: Value = res.json().await.expect("Response was not JSON");
830 let did = body["did"].as_str().expect("No DID").to_string();
831 assert!(did.starts_with("did:plc:"), "Should be did:plc account");
832 let jwt = verify_new_account(&client, &did).await;
833 let res = client
834 .post(format!(
835 "{}/xrpc/com.atproto.server.deactivateAccount",
836 base
837 ))
838 .bearer_auth(&jwt)
839 .json(&json!({ "migratingTo": "https://pds2.example.com" }))
840 .send()
841 .await
842 .expect("Failed to send request");
843 assert_eq!(res.status(), StatusCode::OK);
844 let pool = get_test_db_pool().await;
845 let row = sqlx::query!(
846 r#"SELECT migrated_to_pds, deactivated_at FROM users WHERE did = $1"#,
847 &did
848 )
849 .fetch_one(pool)
850 .await
851 .expect("Failed to query user");
852 assert!(
853 row.migrated_to_pds.is_none(),
854 "migrated_to_pds should NOT be set for did:plc accounts"
855 );
856 assert!(
857 row.deactivated_at.is_some(),
858 "deactivated_at should still be set"
859 );
860 let res = client
861 .get(format!("{}/xrpc/com.atproto.server.getSession", base))
862 .bearer_auth(&jwt)
863 .send()
864 .await
865 .expect("Failed to send request");
866 assert_eq!(res.status(), StatusCode::OK);
867 let body: Value = res.json().await.expect("Response was not JSON");
868 assert_eq!(body["active"], false);
869 assert_eq!(
870 body["status"], "deactivated",
871 "Status should be 'deactivated' not 'migrated' for did:plc"
872 );
873 assert!(
874 body["migratedToPds"].is_null(),
875 "migratedToPds should not be set for did:plc accounts"
876 );
877}