this repo has no description

Rest of lifecycle tests split out to other files

+40
.sqlx/query-2ff22a8c39914689d6cf215ba201fa4ced50b7a003ce01bf7603a7f125113447.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "\n SELECT code, available_uses, created_at, disabled\n FROM invite_codes\n WHERE created_by_user = $1\n ORDER BY created_at DESC\n ", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "code", 9 + "type_info": "Text" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "available_uses", 14 + "type_info": "Int4" 15 + }, 16 + { 17 + "ordinal": 2, 18 + "name": "created_at", 19 + "type_info": "Timestamptz" 20 + }, 21 + { 22 + "ordinal": 3, 23 + "name": "disabled", 24 + "type_info": "Bool" 25 + } 26 + ], 27 + "parameters": { 28 + "Left": [ 29 + "Uuid" 30 + ] 31 + }, 32 + "nullable": [ 33 + false, 34 + false, 35 + false, 36 + true 37 + ] 38 + }, 39 + "hash": "2ff22a8c39914689d6cf215ba201fa4ced50b7a003ce01bf7603a7f125113447" 40 + }
+14
.sqlx/query-3609b5817e4564b824b0c0f4fe32488ee7caed02cee08fb163e4914c5349eb11.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "UPDATE users SET invites_disabled = TRUE WHERE did = $1", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Text" 9 + ] 10 + }, 11 + "nullable": [] 12 + }, 13 + "hash": "3609b5817e4564b824b0c0f4fe32488ee7caed02cee08fb163e4914c5349eb11" 14 + }
+15
.sqlx/query-411a7cff2d43612379903d6343da0761ae5b8b30a2fa1c89afb85047d4fbe3eb.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "UPDATE records SET takedown_ref = $1 WHERE record_cid = $2", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Text", 9 + "Text" 10 + ] 11 + }, 12 + "nullable": [] 13 + }, 14 + "hash": "411a7cff2d43612379903d6343da0761ae5b8b30a2fa1c89afb85047d4fbe3eb" 15 + }
+14
.sqlx/query-413c5b03501a399dca13f345fcae05770517091d73db93966853e944c68ee237.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "UPDATE invite_codes SET disabled = TRUE WHERE created_by_user = $1", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Uuid" 9 + ] 10 + }, 11 + "nullable": [] 12 + }, 13 + "hash": "413c5b03501a399dca13f345fcae05770517091d73db93966853e944c68ee237" 14 + }
+15
.sqlx/query-41d35cebdf29be500e30ef636ad96450620f71087c174e5a74446fcdb29a2ba8.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "UPDATE blobs SET takedown_ref = $1 WHERE cid = $2", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Text", 9 + "Text" 10 + ] 11 + }, 12 + "nullable": [] 13 + }, 14 + "hash": "41d35cebdf29be500e30ef636ad96450620f71087c174e5a74446fcdb29a2ba8" 15 + }
+28
.sqlx/query-5d5442136932d4088873a935c41cb3a683c4771e4fb8c151b3fd5119fb6c1068.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "\n SELECT u.did, icu.used_at\n FROM invite_code_uses icu\n JOIN users u ON icu.used_by_user = u.id\n WHERE icu.code = $1\n ORDER BY icu.used_at DESC\n ", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "did", 9 + "type_info": "Text" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "used_at", 14 + "type_info": "Timestamptz" 15 + } 16 + ], 17 + "parameters": { 18 + "Left": [ 19 + "Text" 20 + ] 21 + }, 22 + "nullable": [ 23 + false, 24 + false 25 + ] 26 + }, 27 + "hash": "5d5442136932d4088873a935c41cb3a683c4771e4fb8c151b3fd5119fb6c1068" 28 + }
+28
.sqlx/query-62942bd21d545eb15bfea4f46378b6c2ebfe12b8bc9e27c63a6c0f77a9105303.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT cid, takedown_ref FROM blobs WHERE cid = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "cid", 9 + "type_info": "Text" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "takedown_ref", 14 + "type_info": "Text" 15 + } 16 + ], 17 + "parameters": { 18 + "Left": [ 19 + "Text" 20 + ] 21 + }, 22 + "nullable": [ 23 + false, 24 + true 25 + ] 26 + }, 27 + "hash": "62942bd21d545eb15bfea4f46378b6c2ebfe12b8bc9e27c63a6c0f77a9105303" 28 + }
+22
.sqlx/query-6819c68a3c06083a826eb94271cc8ff0d4c2bbd33b9051f50a1a46ecc8d3e85b.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT did FROM users WHERE id = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "did", 9 + "type_info": "Text" 10 + } 11 + ], 12 + "parameters": { 13 + "Left": [ 14 + "Uuid" 15 + ] 16 + }, 17 + "nullable": [ 18 + false 19 + ] 20 + }, 21 + "hash": "6819c68a3c06083a826eb94271cc8ff0d4c2bbd33b9051f50a1a46ecc8d3e85b" 22 + }
+14
.sqlx/query-78ed180c33b8f1f7a3adcd3dd0e7e5988ae1dbc2e10009df9fe44fb0fbbe95b3.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "UPDATE users SET invites_disabled = FALSE WHERE did = $1", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Text" 9 + ] 10 + }, 11 + "nullable": [] 12 + }, 13 + "hash": "78ed180c33b8f1f7a3adcd3dd0e7e5988ae1dbc2e10009df9fe44fb0fbbe95b3" 14 + }
+14
.sqlx/query-7b2d1d4ac06063e07a7c7a7d0fb434db08ce312eb2864405d7f96f4e985ed036.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "UPDATE invite_codes SET disabled = TRUE WHERE code = $1", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Text" 9 + ] 10 + }, 11 + "nullable": [] 12 + }, 13 + "hash": "7b2d1d4ac06063e07a7c7a7d0fb434db08ce312eb2864405d7f96f4e985ed036" 14 + }
+34
.sqlx/query-7d1617283733986244b8129cdd14ec1d04510aa73e4ae350a54f57629b9eaff9.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT did, deactivated_at, takedown_ref FROM users WHERE did = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "did", 9 + "type_info": "Text" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "deactivated_at", 14 + "type_info": "Timestamptz" 15 + }, 16 + { 17 + "ordinal": 2, 18 + "name": "takedown_ref", 19 + "type_info": "Text" 20 + } 21 + ], 22 + "parameters": { 23 + "Left": [ 24 + "Text" 25 + ] 26 + }, 27 + "nullable": [ 28 + false, 29 + true, 30 + true 31 + ] 32 + }, 33 + "hash": "7d1617283733986244b8129cdd14ec1d04510aa73e4ae350a54f57629b9eaff9" 34 + }
+16
.sqlx/query-bbe639bb24cc1bb3cc144baae263e7e3411e185bf7c91751ee1046c64a81df52.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Text", 9 + "Int4", 10 + "Uuid" 11 + ] 12 + }, 13 + "nullable": [] 14 + }, 15 + "hash": "bbe639bb24cc1bb3cc144baae263e7e3411e185bf7c91751ee1046c64a81df52" 16 + }
+15
.sqlx/query-cd25ddc034a51748f699e2fcd1312691123aee9904eb2ee4073ed0f2c8c49bf9.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "UPDATE users SET takedown_ref = $1 WHERE did = $2", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Left": [ 8 + "Text", 9 + "Text" 10 + ] 11 + }, 12 + "nullable": [] 13 + }, 14 + "hash": "cd25ddc034a51748f699e2fcd1312691123aee9904eb2ee4073ed0f2c8c49bf9" 15 + }
+22
.sqlx/query-da0e9a9edad3895ed5015b52335f5a0256e7bdc6c79e6faa927414d68800404c.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT invites_disabled FROM users WHERE did = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "invites_disabled", 9 + "type_info": "Bool" 10 + } 11 + ], 12 + "parameters": { 13 + "Left": [ 14 + "Text" 15 + ] 16 + }, 17 + "nullable": [ 18 + true 19 + ] 20 + }, 21 + "hash": "da0e9a9edad3895ed5015b52335f5a0256e7bdc6c79e6faa927414d68800404c" 22 + }
+28
.sqlx/query-fbc8ab04fe5e06d6e6de9a4eeaabee8af9ee887812bcfe5893df1c7e682747c1.json
···
··· 1 + { 2 + "db_name": "PostgreSQL", 3 + "query": "SELECT r.id, r.takedown_ref FROM records r WHERE r.record_cid = $1", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "ordinal": 0, 8 + "name": "id", 9 + "type_info": "Uuid" 10 + }, 11 + { 12 + "ordinal": 1, 13 + "name": "takedown_ref", 14 + "type_info": "Text" 15 + } 16 + ], 17 + "parameters": { 18 + "Left": [ 19 + "Text" 20 + ] 21 + }, 22 + "nullable": [ 23 + false, 24 + true 25 + ] 26 + }, 27 + "hash": "fbc8ab04fe5e06d6e6de9a4eeaabee8af9ee887812bcfe5893df1c7e682747c1" 28 + }
-2414
tests/lifecycle.rs
··· 1 - mod common; 2 - use common::*; 3 - 4 - use base64::Engine; 5 - use chrono::Utc; 6 - use reqwest::{self, StatusCode, header}; 7 - use serde_json::{Value, json}; 8 - use std::time::Duration; 9 - 10 - async fn setup_new_user(handle_prefix: &str) -> (String, String) { 11 - let client = client(); 12 - let ts = Utc::now().timestamp_millis(); 13 - let handle = format!("{}-{}.test", handle_prefix, ts); 14 - let email = format!("{}-{}@test.com", handle_prefix, ts); 15 - let password = "e2e-password-123"; 16 - 17 - let create_account_payload = json!({ 18 - "handle": handle, 19 - "email": email, 20 - "password": password 21 - }); 22 - let create_res = client 23 - .post(format!( 24 - "{}/xrpc/com.atproto.server.createAccount", 25 - base_url().await 26 - )) 27 - .json(&create_account_payload) 28 - .send() 29 - .await 30 - .expect("setup_new_user: Failed to send createAccount"); 31 - 32 - if create_res.status() != reqwest::StatusCode::OK { 33 - panic!( 34 - "setup_new_user: Failed to create account: {:?}", 35 - create_res.text().await 36 - ); 37 - } 38 - 39 - let create_body: Value = create_res 40 - .json() 41 - .await 42 - .expect("setup_new_user: createAccount response was not JSON"); 43 - 44 - let new_did = create_body["did"] 45 - .as_str() 46 - .expect("setup_new_user: Response had no DID") 47 - .to_string(); 48 - let new_jwt = create_body["accessJwt"] 49 - .as_str() 50 - .expect("setup_new_user: Response had no accessJwt") 51 - .to_string(); 52 - 53 - (new_did, new_jwt) 54 - } 55 - 56 - #[tokio::test] 57 - async fn test_post_crud_lifecycle() { 58 - let client = client(); 59 - let (did, jwt) = setup_new_user("lifecycle-crud").await; 60 - let collection = "app.bsky.feed.post"; 61 - 62 - let rkey = format!("e2e_lifecycle_{}", Utc::now().timestamp_millis()); 63 - let now = Utc::now().to_rfc3339(); 64 - 65 - let original_text = "Hello from the lifecycle test!"; 66 - let create_payload = json!({ 67 - "repo": did, 68 - "collection": collection, 69 - "rkey": rkey, 70 - "record": { 71 - "$type": collection, 72 - "text": original_text, 73 - "createdAt": now 74 - } 75 - }); 76 - 77 - let create_res = client 78 - .post(format!( 79 - "{}/xrpc/com.atproto.repo.putRecord", 80 - base_url().await 81 - )) 82 - .bearer_auth(&jwt) 83 - .json(&create_payload) 84 - .send() 85 - .await 86 - .expect("Failed to send create request"); 87 - 88 - if create_res.status() != reqwest::StatusCode::OK { 89 - let status = create_res.status(); 90 - let body = create_res 91 - .text() 92 - .await 93 - .unwrap_or_else(|_| "Could not get body".to_string()); 94 - panic!( 95 - "Failed to create record. Status: {}, Body: {}", 96 - status, body 97 - ); 98 - } 99 - 100 - let create_body: Value = create_res 101 - .json() 102 - .await 103 - .expect("create response was not JSON"); 104 - let uri = create_body["uri"].as_str().unwrap(); 105 - 106 - let params = [ 107 - ("repo", did.as_str()), 108 - ("collection", collection), 109 - ("rkey", &rkey), 110 - ]; 111 - let get_res = client 112 - .get(format!( 113 - "{}/xrpc/com.atproto.repo.getRecord", 114 - base_url().await 115 - )) 116 - .query(&params) 117 - .send() 118 - .await 119 - .expect("Failed to send get request"); 120 - 121 - assert_eq!( 122 - get_res.status(), 123 - reqwest::StatusCode::OK, 124 - "Failed to get record after create" 125 - ); 126 - let get_body: Value = get_res.json().await.expect("get response was not JSON"); 127 - assert_eq!(get_body["uri"], uri); 128 - assert_eq!(get_body["value"]["text"], original_text); 129 - 130 - let updated_text = "This post has been updated."; 131 - let update_payload = json!({ 132 - "repo": did, 133 - "collection": collection, 134 - "rkey": rkey, 135 - "record": { 136 - "$type": collection, 137 - "text": updated_text, 138 - "createdAt": now 139 - } 140 - }); 141 - 142 - let update_res = client 143 - .post(format!( 144 - "{}/xrpc/com.atproto.repo.putRecord", 145 - base_url().await 146 - )) 147 - .bearer_auth(&jwt) 148 - .json(&update_payload) 149 - .send() 150 - .await 151 - .expect("Failed to send update request"); 152 - 153 - assert_eq!( 154 - update_res.status(), 155 - reqwest::StatusCode::OK, 156 - "Failed to update record" 157 - ); 158 - 159 - let get_updated_res = client 160 - .get(format!( 161 - "{}/xrpc/com.atproto.repo.getRecord", 162 - base_url().await 163 - )) 164 - .query(&params) 165 - .send() 166 - .await 167 - .expect("Failed to send get-after-update request"); 168 - 169 - assert_eq!( 170 - get_updated_res.status(), 171 - reqwest::StatusCode::OK, 172 - "Failed to get record after update" 173 - ); 174 - let get_updated_body: Value = get_updated_res 175 - .json() 176 - .await 177 - .expect("get-updated response was not JSON"); 178 - assert_eq!( 179 - get_updated_body["value"]["text"], updated_text, 180 - "Text was not updated" 181 - ); 182 - 183 - let delete_payload = json!({ 184 - "repo": did, 185 - "collection": collection, 186 - "rkey": rkey 187 - }); 188 - 189 - let delete_res = client 190 - .post(format!( 191 - "{}/xrpc/com.atproto.repo.deleteRecord", 192 - base_url().await 193 - )) 194 - .bearer_auth(&jwt) 195 - .json(&delete_payload) 196 - .send() 197 - .await 198 - .expect("Failed to send delete request"); 199 - 200 - assert_eq!( 201 - delete_res.status(), 202 - reqwest::StatusCode::OK, 203 - "Failed to delete record" 204 - ); 205 - 206 - let get_deleted_res = client 207 - .get(format!( 208 - "{}/xrpc/com.atproto.repo.getRecord", 209 - base_url().await 210 - )) 211 - .query(&params) 212 - .send() 213 - .await 214 - .expect("Failed to send get-after-delete request"); 215 - 216 - assert_eq!( 217 - get_deleted_res.status(), 218 - reqwest::StatusCode::NOT_FOUND, 219 - "Record was found, but it should be deleted" 220 - ); 221 - } 222 - 223 - #[tokio::test] 224 - async fn test_record_update_conflict_lifecycle() { 225 - let client = client(); 226 - let (user_did, user_jwt) = setup_new_user("user-conflict").await; 227 - 228 - let profile_payload = json!({ 229 - "repo": user_did, 230 - "collection": "app.bsky.actor.profile", 231 - "rkey": "self", 232 - "record": { 233 - "$type": "app.bsky.actor.profile", 234 - "displayName": "Original Name" 235 - } 236 - }); 237 - let create_res = client 238 - .post(format!( 239 - "{}/xrpc/com.atproto.repo.putRecord", 240 - base_url().await 241 - )) 242 - .bearer_auth(&user_jwt) 243 - .json(&profile_payload) 244 - .send() 245 - .await 246 - .expect("create profile failed"); 247 - 248 - if create_res.status() != reqwest::StatusCode::OK { 249 - return; 250 - } 251 - 252 - let get_res = client 253 - .get(format!( 254 - "{}/xrpc/com.atproto.repo.getRecord", 255 - base_url().await 256 - )) 257 - .query(&[ 258 - ("repo", &user_did), 259 - ("collection", &"app.bsky.actor.profile".to_string()), 260 - ("rkey", &"self".to_string()), 261 - ]) 262 - .send() 263 - .await 264 - .expect("getRecord failed"); 265 - let get_body: Value = get_res.json().await.expect("getRecord not json"); 266 - let cid_v1 = get_body["cid"] 267 - .as_str() 268 - .expect("Profile v1 had no CID") 269 - .to_string(); 270 - 271 - let update_payload_v2 = json!({ 272 - "repo": user_did, 273 - "collection": "app.bsky.actor.profile", 274 - "rkey": "self", 275 - "record": { 276 - "$type": "app.bsky.actor.profile", 277 - "displayName": "Updated Name (v2)" 278 - }, 279 - "swapRecord": cid_v1 280 - }); 281 - let update_res_v2 = client 282 - .post(format!( 283 - "{}/xrpc/com.atproto.repo.putRecord", 284 - base_url().await 285 - )) 286 - .bearer_auth(&user_jwt) 287 - .json(&update_payload_v2) 288 - .send() 289 - .await 290 - .expect("putRecord v2 failed"); 291 - assert_eq!( 292 - update_res_v2.status(), 293 - reqwest::StatusCode::OK, 294 - "v2 update failed" 295 - ); 296 - let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json"); 297 - let cid_v2 = update_body_v2["cid"] 298 - .as_str() 299 - .expect("v2 response had no CID") 300 - .to_string(); 301 - 302 - let update_payload_v3_stale = json!({ 303 - "repo": user_did, 304 - "collection": "app.bsky.actor.profile", 305 - "rkey": "self", 306 - "record": { 307 - "$type": "app.bsky.actor.profile", 308 - "displayName": "Stale Update (v3)" 309 - }, 310 - "swapRecord": cid_v1 311 - }); 312 - let update_res_v3_stale = client 313 - .post(format!( 314 - "{}/xrpc/com.atproto.repo.putRecord", 315 - base_url().await 316 - )) 317 - .bearer_auth(&user_jwt) 318 - .json(&update_payload_v3_stale) 319 - .send() 320 - .await 321 - .expect("putRecord v3 (stale) failed"); 322 - 323 - assert_eq!( 324 - update_res_v3_stale.status(), 325 - reqwest::StatusCode::CONFLICT, 326 - "Stale update did not cause a 409 Conflict" 327 - ); 328 - 329 - let update_payload_v3_good = json!({ 330 - "repo": user_did, 331 - "collection": "app.bsky.actor.profile", 332 - "rkey": "self", 333 - "record": { 334 - "$type": "app.bsky.actor.profile", 335 - "displayName": "Good Update (v3)" 336 - }, 337 - "swapRecord": cid_v2 338 - }); 339 - let update_res_v3_good = client 340 - .post(format!( 341 - "{}/xrpc/com.atproto.repo.putRecord", 342 - base_url().await 343 - )) 344 - .bearer_auth(&user_jwt) 345 - .json(&update_payload_v3_good) 346 - .send() 347 - .await 348 - .expect("putRecord v3 (good) failed"); 349 - 350 - assert_eq!( 351 - update_res_v3_good.status(), 352 - reqwest::StatusCode::OK, 353 - "v3 (good) update failed" 354 - ); 355 - } 356 - 357 - async fn create_post( 358 - client: &reqwest::Client, 359 - did: &str, 360 - jwt: &str, 361 - text: &str, 362 - ) -> (String, String) { 363 - let collection = "app.bsky.feed.post"; 364 - let rkey = format!("e2e_social_{}", Utc::now().timestamp_millis()); 365 - let now = Utc::now().to_rfc3339(); 366 - 367 - let create_payload = json!({ 368 - "repo": did, 369 - "collection": collection, 370 - "rkey": rkey, 371 - "record": { 372 - "$type": collection, 373 - "text": text, 374 - "createdAt": now 375 - } 376 - }); 377 - 378 - let create_res = client 379 - .post(format!( 380 - "{}/xrpc/com.atproto.repo.putRecord", 381 - base_url().await 382 - )) 383 - .bearer_auth(jwt) 384 - .json(&create_payload) 385 - .send() 386 - .await 387 - .expect("Failed to send create post request"); 388 - 389 - assert_eq!( 390 - create_res.status(), 391 - reqwest::StatusCode::OK, 392 - "Failed to create post record" 393 - ); 394 - let create_body: Value = create_res 395 - .json() 396 - .await 397 - .expect("create post response was not JSON"); 398 - let uri = create_body["uri"].as_str().unwrap().to_string(); 399 - let cid = create_body["cid"].as_str().unwrap().to_string(); 400 - (uri, cid) 401 - } 402 - 403 - async fn create_follow( 404 - client: &reqwest::Client, 405 - follower_did: &str, 406 - follower_jwt: &str, 407 - followee_did: &str, 408 - ) -> (String, String) { 409 - let collection = "app.bsky.graph.follow"; 410 - let rkey = format!("e2e_follow_{}", Utc::now().timestamp_millis()); 411 - let now = Utc::now().to_rfc3339(); 412 - 413 - let create_payload = json!({ 414 - "repo": follower_did, 415 - "collection": collection, 416 - "rkey": rkey, 417 - "record": { 418 - "$type": collection, 419 - "subject": followee_did, 420 - "createdAt": now 421 - } 422 - }); 423 - 424 - let create_res = client 425 - .post(format!( 426 - "{}/xrpc/com.atproto.repo.putRecord", 427 - base_url().await 428 - )) 429 - .bearer_auth(follower_jwt) 430 - .json(&create_payload) 431 - .send() 432 - .await 433 - .expect("Failed to send create follow request"); 434 - 435 - assert_eq!( 436 - create_res.status(), 437 - reqwest::StatusCode::OK, 438 - "Failed to create follow record" 439 - ); 440 - let create_body: Value = create_res 441 - .json() 442 - .await 443 - .expect("create follow response was not JSON"); 444 - let uri = create_body["uri"].as_str().unwrap().to_string(); 445 - let cid = create_body["cid"].as_str().unwrap().to_string(); 446 - (uri, cid) 447 - } 448 - 449 - #[tokio::test] 450 - async fn test_social_flow_lifecycle() { 451 - let client = client(); 452 - 453 - let (alice_did, alice_jwt) = setup_new_user("alice-social").await; 454 - let (bob_did, bob_jwt) = setup_new_user("bob-social").await; 455 - 456 - let (post1_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's first post!").await; 457 - 458 - create_follow(&client, &bob_did, &bob_jwt, &alice_did).await; 459 - 460 - tokio::time::sleep(Duration::from_secs(1)).await; 461 - 462 - let timeline_res_1 = client 463 - .get(format!( 464 - "{}/xrpc/app.bsky.feed.getTimeline", 465 - base_url().await 466 - )) 467 - .bearer_auth(&bob_jwt) 468 - .send() 469 - .await 470 - .expect("Failed to get timeline (1)"); 471 - 472 - assert_eq!( 473 - timeline_res_1.status(), 474 - reqwest::StatusCode::OK, 475 - "Failed to get timeline (1)" 476 - ); 477 - let timeline_body_1: Value = timeline_res_1.json().await.expect("Timeline (1) not JSON"); 478 - let feed_1 = timeline_body_1["feed"].as_array().unwrap(); 479 - assert_eq!(feed_1.len(), 1, "Timeline should have 1 post"); 480 - assert_eq!( 481 - feed_1[0]["post"]["uri"], post1_uri, 482 - "Post URI mismatch in timeline (1)" 483 - ); 484 - 485 - let (post2_uri, _) = create_post( 486 - &client, 487 - &alice_did, 488 - &alice_jwt, 489 - "Alice's second post, so exciting!", 490 - ) 491 - .await; 492 - 493 - tokio::time::sleep(Duration::from_secs(1)).await; 494 - 495 - let timeline_res_2 = client 496 - .get(format!( 497 - "{}/xrpc/app.bsky.feed.getTimeline", 498 - base_url().await 499 - )) 500 - .bearer_auth(&bob_jwt) 501 - .send() 502 - .await 503 - .expect("Failed to get timeline (2)"); 504 - 505 - assert_eq!( 506 - timeline_res_2.status(), 507 - reqwest::StatusCode::OK, 508 - "Failed to get timeline (2)" 509 - ); 510 - let timeline_body_2: Value = timeline_res_2.json().await.expect("Timeline (2) not JSON"); 511 - let feed_2 = timeline_body_2["feed"].as_array().unwrap(); 512 - assert_eq!(feed_2.len(), 2, "Timeline should have 2 posts"); 513 - assert_eq!( 514 - feed_2[0]["post"]["uri"], post2_uri, 515 - "Post 2 should be first" 516 - ); 517 - assert_eq!( 518 - feed_2[1]["post"]["uri"], post1_uri, 519 - "Post 1 should be second" 520 - ); 521 - 522 - let delete_payload = json!({ 523 - "repo": alice_did, 524 - "collection": "app.bsky.feed.post", 525 - "rkey": post1_uri.split('/').last().unwrap() 526 - }); 527 - let delete_res = client 528 - .post(format!( 529 - "{}/xrpc/com.atproto.repo.deleteRecord", 530 - base_url().await 531 - )) 532 - .bearer_auth(&alice_jwt) 533 - .json(&delete_payload) 534 - .send() 535 - .await 536 - .expect("Failed to send delete request"); 537 - assert_eq!( 538 - delete_res.status(), 539 - reqwest::StatusCode::OK, 540 - "Failed to delete record" 541 - ); 542 - 543 - tokio::time::sleep(Duration::from_secs(1)).await; 544 - 545 - let timeline_res_3 = client 546 - .get(format!( 547 - "{}/xrpc/app.bsky.feed.getTimeline", 548 - base_url().await 549 - )) 550 - .bearer_auth(&bob_jwt) 551 - .send() 552 - .await 553 - .expect("Failed to get timeline (3)"); 554 - 555 - assert_eq!( 556 - timeline_res_3.status(), 557 - reqwest::StatusCode::OK, 558 - "Failed to get timeline (3)" 559 - ); 560 - let timeline_body_3: Value = timeline_res_3.json().await.expect("Timeline (3) not JSON"); 561 - let feed_3 = timeline_body_3["feed"].as_array().unwrap(); 562 - assert_eq!(feed_3.len(), 1, "Timeline should have 1 post after delete"); 563 - assert_eq!( 564 - feed_3[0]["post"]["uri"], post2_uri, 565 - "Only post 2 should remain" 566 - ); 567 - } 568 - 569 - #[tokio::test] 570 - async fn test_session_lifecycle_wrong_password() { 571 - let client = client(); 572 - let (_, _) = setup_new_user("session-wrong-pw").await; 573 - 574 - let login_payload = json!({ 575 - "identifier": format!("session-wrong-pw-{}.test", Utc::now().timestamp_millis()), 576 - "password": "wrong-password" 577 - }); 578 - 579 - let res = client 580 - .post(format!( 581 - "{}/xrpc/com.atproto.server.createSession", 582 - base_url().await 583 - )) 584 - .json(&login_payload) 585 - .send() 586 - .await 587 - .expect("Failed to send request"); 588 - 589 - assert!( 590 - res.status() == StatusCode::UNAUTHORIZED || res.status() == StatusCode::BAD_REQUEST, 591 - "Expected 401 or 400 for wrong password, got {}", 592 - res.status() 593 - ); 594 - } 595 - 596 - #[tokio::test] 597 - async fn test_session_lifecycle_multiple_sessions() { 598 - let client = client(); 599 - let ts = Utc::now().timestamp_millis(); 600 - let handle = format!("multi-session-{}.test", ts); 601 - let email = format!("multi-session-{}@test.com", ts); 602 - let password = "multi-session-pw"; 603 - 604 - let create_payload = json!({ 605 - "handle": handle, 606 - "email": email, 607 - "password": password 608 - }); 609 - let create_res = client 610 - .post(format!( 611 - "{}/xrpc/com.atproto.server.createAccount", 612 - base_url().await 613 - )) 614 - .json(&create_payload) 615 - .send() 616 - .await 617 - .expect("Failed to create account"); 618 - assert_eq!(create_res.status(), StatusCode::OK); 619 - 620 - let login_payload = json!({ 621 - "identifier": handle, 622 - "password": password 623 - }); 624 - 625 - let session1_res = client 626 - .post(format!( 627 - "{}/xrpc/com.atproto.server.createSession", 628 - base_url().await 629 - )) 630 - .json(&login_payload) 631 - .send() 632 - .await 633 - .expect("Failed session 1"); 634 - assert_eq!(session1_res.status(), StatusCode::OK); 635 - let session1: Value = session1_res.json().await.unwrap(); 636 - let jwt1 = session1["accessJwt"].as_str().unwrap(); 637 - 638 - let session2_res = client 639 - .post(format!( 640 - "{}/xrpc/com.atproto.server.createSession", 641 - base_url().await 642 - )) 643 - .json(&login_payload) 644 - .send() 645 - .await 646 - .expect("Failed session 2"); 647 - assert_eq!(session2_res.status(), StatusCode::OK); 648 - let session2: Value = session2_res.json().await.unwrap(); 649 - let jwt2 = session2["accessJwt"].as_str().unwrap(); 650 - 651 - assert_ne!(jwt1, jwt2, "Sessions should have different tokens"); 652 - 653 - let get1 = client 654 - .get(format!( 655 - "{}/xrpc/com.atproto.server.getSession", 656 - base_url().await 657 - )) 658 - .bearer_auth(jwt1) 659 - .send() 660 - .await 661 - .expect("Failed getSession 1"); 662 - assert_eq!(get1.status(), StatusCode::OK); 663 - 664 - let get2 = client 665 - .get(format!( 666 - "{}/xrpc/com.atproto.server.getSession", 667 - base_url().await 668 - )) 669 - .bearer_auth(jwt2) 670 - .send() 671 - .await 672 - .expect("Failed getSession 2"); 673 - assert_eq!(get2.status(), StatusCode::OK); 674 - } 675 - 676 - #[tokio::test] 677 - async fn test_session_lifecycle_refresh_invalidates_old() { 678 - let client = client(); 679 - let ts = Utc::now().timestamp_millis(); 680 - let handle = format!("refresh-inv-{}.test", ts); 681 - let email = format!("refresh-inv-{}@test.com", ts); 682 - let password = "refresh-inv-pw"; 683 - 684 - let create_payload = json!({ 685 - "handle": handle, 686 - "email": email, 687 - "password": password 688 - }); 689 - client 690 - .post(format!( 691 - "{}/xrpc/com.atproto.server.createAccount", 692 - base_url().await 693 - )) 694 - .json(&create_payload) 695 - .send() 696 - .await 697 - .expect("Failed to create account"); 698 - 699 - let login_payload = json!({ 700 - "identifier": handle, 701 - "password": password 702 - }); 703 - let login_res = client 704 - .post(format!( 705 - "{}/xrpc/com.atproto.server.createSession", 706 - base_url().await 707 - )) 708 - .json(&login_payload) 709 - .send() 710 - .await 711 - .expect("Failed login"); 712 - let login_body: Value = login_res.json().await.unwrap(); 713 - let refresh_jwt = login_body["refreshJwt"].as_str().unwrap().to_string(); 714 - 715 - let refresh_res = client 716 - .post(format!( 717 - "{}/xrpc/com.atproto.server.refreshSession", 718 - base_url().await 719 - )) 720 - .bearer_auth(&refresh_jwt) 721 - .send() 722 - .await 723 - .expect("Failed first refresh"); 724 - assert_eq!(refresh_res.status(), StatusCode::OK); 725 - let refresh_body: Value = refresh_res.json().await.unwrap(); 726 - let new_refresh_jwt = refresh_body["refreshJwt"].as_str().unwrap(); 727 - 728 - assert_ne!(refresh_jwt, new_refresh_jwt, "Refresh tokens should differ"); 729 - 730 - let reuse_res = client 731 - .post(format!( 732 - "{}/xrpc/com.atproto.server.refreshSession", 733 - base_url().await 734 - )) 735 - .bearer_auth(&refresh_jwt) 736 - .send() 737 - .await 738 - .expect("Failed reuse attempt"); 739 - 740 - assert!( 741 - reuse_res.status() == StatusCode::UNAUTHORIZED || reuse_res.status() == StatusCode::BAD_REQUEST, 742 - "Old refresh token should be invalid after use" 743 - ); 744 - } 745 - 746 - async fn create_like( 747 - client: &reqwest::Client, 748 - liker_did: &str, 749 - liker_jwt: &str, 750 - subject_uri: &str, 751 - subject_cid: &str, 752 - ) -> (String, String) { 753 - let collection = "app.bsky.feed.like"; 754 - let rkey = format!("e2e_like_{}", Utc::now().timestamp_millis()); 755 - let now = Utc::now().to_rfc3339(); 756 - 757 - let payload = json!({ 758 - "repo": liker_did, 759 - "collection": collection, 760 - "rkey": rkey, 761 - "record": { 762 - "$type": collection, 763 - "subject": { 764 - "uri": subject_uri, 765 - "cid": subject_cid 766 - }, 767 - "createdAt": now 768 - } 769 - }); 770 - 771 - let res = client 772 - .post(format!( 773 - "{}/xrpc/com.atproto.repo.putRecord", 774 - base_url().await 775 - )) 776 - .bearer_auth(liker_jwt) 777 - .json(&payload) 778 - .send() 779 - .await 780 - .expect("Failed to create like"); 781 - 782 - assert_eq!(res.status(), StatusCode::OK, "Failed to create like"); 783 - let body: Value = res.json().await.expect("Like response not JSON"); 784 - ( 785 - body["uri"].as_str().unwrap().to_string(), 786 - body["cid"].as_str().unwrap().to_string(), 787 - ) 788 - } 789 - 790 - async fn create_repost( 791 - client: &reqwest::Client, 792 - reposter_did: &str, 793 - reposter_jwt: &str, 794 - subject_uri: &str, 795 - subject_cid: &str, 796 - ) -> (String, String) { 797 - let collection = "app.bsky.feed.repost"; 798 - let rkey = format!("e2e_repost_{}", Utc::now().timestamp_millis()); 799 - let now = Utc::now().to_rfc3339(); 800 - 801 - let payload = json!({ 802 - "repo": reposter_did, 803 - "collection": collection, 804 - "rkey": rkey, 805 - "record": { 806 - "$type": collection, 807 - "subject": { 808 - "uri": subject_uri, 809 - "cid": subject_cid 810 - }, 811 - "createdAt": now 812 - } 813 - }); 814 - 815 - let res = client 816 - .post(format!( 817 - "{}/xrpc/com.atproto.repo.putRecord", 818 - base_url().await 819 - )) 820 - .bearer_auth(reposter_jwt) 821 - .json(&payload) 822 - .send() 823 - .await 824 - .expect("Failed to create repost"); 825 - 826 - assert_eq!(res.status(), StatusCode::OK, "Failed to create repost"); 827 - let body: Value = res.json().await.expect("Repost response not JSON"); 828 - ( 829 - body["uri"].as_str().unwrap().to_string(), 830 - body["cid"].as_str().unwrap().to_string(), 831 - ) 832 - } 833 - 834 - #[tokio::test] 835 - async fn test_profile_lifecycle() { 836 - let client = client(); 837 - let (did, jwt) = setup_new_user("profile-lifecycle").await; 838 - 839 - let profile_payload = json!({ 840 - "repo": did, 841 - "collection": "app.bsky.actor.profile", 842 - "rkey": "self", 843 - "record": { 844 - "$type": "app.bsky.actor.profile", 845 - "displayName": "Test User", 846 - "description": "A test profile for lifecycle testing" 847 - } 848 - }); 849 - 850 - let create_res = client 851 - .post(format!( 852 - "{}/xrpc/com.atproto.repo.putRecord", 853 - base_url().await 854 - )) 855 - .bearer_auth(&jwt) 856 - .json(&profile_payload) 857 - .send() 858 - .await 859 - .expect("Failed to create profile"); 860 - 861 - assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile"); 862 - let create_body: Value = create_res.json().await.unwrap(); 863 - let initial_cid = create_body["cid"].as_str().unwrap().to_string(); 864 - 865 - let get_res = client 866 - .get(format!( 867 - "{}/xrpc/com.atproto.repo.getRecord", 868 - base_url().await 869 - )) 870 - .query(&[ 871 - ("repo", did.as_str()), 872 - ("collection", "app.bsky.actor.profile"), 873 - ("rkey", "self"), 874 - ]) 875 - .send() 876 - .await 877 - .expect("Failed to get profile"); 878 - 879 - assert_eq!(get_res.status(), StatusCode::OK); 880 - let get_body: Value = get_res.json().await.unwrap(); 881 - assert_eq!(get_body["value"]["displayName"], "Test User"); 882 - assert_eq!(get_body["value"]["description"], "A test profile for lifecycle testing"); 883 - 884 - let update_payload = json!({ 885 - "repo": did, 886 - "collection": "app.bsky.actor.profile", 887 - "rkey": "self", 888 - "record": { 889 - "$type": "app.bsky.actor.profile", 890 - "displayName": "Updated User", 891 - "description": "Profile has been updated" 892 - }, 893 - "swapRecord": initial_cid 894 - }); 895 - 896 - let update_res = client 897 - .post(format!( 898 - "{}/xrpc/com.atproto.repo.putRecord", 899 - base_url().await 900 - )) 901 - .bearer_auth(&jwt) 902 - .json(&update_payload) 903 - .send() 904 - .await 905 - .expect("Failed to update profile"); 906 - 907 - assert_eq!(update_res.status(), StatusCode::OK, "Failed to update profile"); 908 - 909 - let get_updated_res = client 910 - .get(format!( 911 - "{}/xrpc/com.atproto.repo.getRecord", 912 - base_url().await 913 - )) 914 - .query(&[ 915 - ("repo", did.as_str()), 916 - ("collection", "app.bsky.actor.profile"), 917 - ("rkey", "self"), 918 - ]) 919 - .send() 920 - .await 921 - .expect("Failed to get updated profile"); 922 - 923 - let updated_body: Value = get_updated_res.json().await.unwrap(); 924 - assert_eq!(updated_body["value"]["displayName"], "Updated User"); 925 - } 926 - 927 - #[tokio::test] 928 - async fn test_reply_thread_lifecycle() { 929 - let client = client(); 930 - 931 - let (alice_did, alice_jwt) = setup_new_user("alice-thread").await; 932 - let (bob_did, bob_jwt) = setup_new_user("bob-thread").await; 933 - 934 - let (root_uri, root_cid) = create_post(&client, &alice_did, &alice_jwt, "This is the root post").await; 935 - 936 - tokio::time::sleep(Duration::from_millis(100)).await; 937 - 938 - let reply_collection = "app.bsky.feed.post"; 939 - let reply_rkey = format!("e2e_reply_{}", Utc::now().timestamp_millis()); 940 - let now = Utc::now().to_rfc3339(); 941 - 942 - let reply_payload = json!({ 943 - "repo": bob_did, 944 - "collection": reply_collection, 945 - "rkey": reply_rkey, 946 - "record": { 947 - "$type": reply_collection, 948 - "text": "This is Bob's reply to Alice", 949 - "createdAt": now, 950 - "reply": { 951 - "root": { 952 - "uri": root_uri, 953 - "cid": root_cid 954 - }, 955 - "parent": { 956 - "uri": root_uri, 957 - "cid": root_cid 958 - } 959 - } 960 - } 961 - }); 962 - 963 - let reply_res = client 964 - .post(format!( 965 - "{}/xrpc/com.atproto.repo.putRecord", 966 - base_url().await 967 - )) 968 - .bearer_auth(&bob_jwt) 969 - .json(&reply_payload) 970 - .send() 971 - .await 972 - .expect("Failed to create reply"); 973 - 974 - assert_eq!(reply_res.status(), StatusCode::OK, "Failed to create reply"); 975 - let reply_body: Value = reply_res.json().await.unwrap(); 976 - let reply_uri = reply_body["uri"].as_str().unwrap(); 977 - let reply_cid = reply_body["cid"].as_str().unwrap(); 978 - 979 - let get_reply_res = client 980 - .get(format!( 981 - "{}/xrpc/com.atproto.repo.getRecord", 982 - base_url().await 983 - )) 984 - .query(&[ 985 - ("repo", bob_did.as_str()), 986 - ("collection", reply_collection), 987 - ("rkey", reply_rkey.as_str()), 988 - ]) 989 - .send() 990 - .await 991 - .expect("Failed to get reply"); 992 - 993 - assert_eq!(get_reply_res.status(), StatusCode::OK); 994 - let reply_record: Value = get_reply_res.json().await.unwrap(); 995 - assert_eq!(reply_record["value"]["reply"]["root"]["uri"], root_uri); 996 - assert_eq!(reply_record["value"]["reply"]["parent"]["uri"], root_uri); 997 - 998 - tokio::time::sleep(Duration::from_millis(100)).await; 999 - 1000 - let nested_reply_rkey = format!("e2e_nested_reply_{}", Utc::now().timestamp_millis()); 1001 - let nested_payload = json!({ 1002 - "repo": alice_did, 1003 - "collection": reply_collection, 1004 - "rkey": nested_reply_rkey, 1005 - "record": { 1006 - "$type": reply_collection, 1007 - "text": "Alice replies to Bob's reply", 1008 - "createdAt": Utc::now().to_rfc3339(), 1009 - "reply": { 1010 - "root": { 1011 - "uri": root_uri, 1012 - "cid": root_cid 1013 - }, 1014 - "parent": { 1015 - "uri": reply_uri, 1016 - "cid": reply_cid 1017 - } 1018 - } 1019 - } 1020 - }); 1021 - 1022 - let nested_res = client 1023 - .post(format!( 1024 - "{}/xrpc/com.atproto.repo.putRecord", 1025 - base_url().await 1026 - )) 1027 - .bearer_auth(&alice_jwt) 1028 - .json(&nested_payload) 1029 - .send() 1030 - .await 1031 - .expect("Failed to create nested reply"); 1032 - 1033 - assert_eq!(nested_res.status(), StatusCode::OK, "Failed to create nested reply"); 1034 - } 1035 - 1036 - #[tokio::test] 1037 - async fn test_like_lifecycle() { 1038 - let client = client(); 1039 - 1040 - let (alice_did, alice_jwt) = setup_new_user("alice-like").await; 1041 - let (bob_did, bob_jwt) = setup_new_user("bob-like").await; 1042 - 1043 - let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Like this post!").await; 1044 - 1045 - let (like_uri, _) = create_like(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await; 1046 - 1047 - let like_rkey = like_uri.split('/').last().unwrap(); 1048 - let get_like_res = client 1049 - .get(format!( 1050 - "{}/xrpc/com.atproto.repo.getRecord", 1051 - base_url().await 1052 - )) 1053 - .query(&[ 1054 - ("repo", bob_did.as_str()), 1055 - ("collection", "app.bsky.feed.like"), 1056 - ("rkey", like_rkey), 1057 - ]) 1058 - .send() 1059 - .await 1060 - .expect("Failed to get like"); 1061 - 1062 - assert_eq!(get_like_res.status(), StatusCode::OK); 1063 - let like_body: Value = get_like_res.json().await.unwrap(); 1064 - assert_eq!(like_body["value"]["subject"]["uri"], post_uri); 1065 - 1066 - let delete_payload = json!({ 1067 - "repo": bob_did, 1068 - "collection": "app.bsky.feed.like", 1069 - "rkey": like_rkey 1070 - }); 1071 - 1072 - let delete_res = client 1073 - .post(format!( 1074 - "{}/xrpc/com.atproto.repo.deleteRecord", 1075 - base_url().await 1076 - )) 1077 - .bearer_auth(&bob_jwt) 1078 - .json(&delete_payload) 1079 - .send() 1080 - .await 1081 - .expect("Failed to delete like"); 1082 - 1083 - assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete like"); 1084 - 1085 - let get_deleted_res = client 1086 - .get(format!( 1087 - "{}/xrpc/com.atproto.repo.getRecord", 1088 - base_url().await 1089 - )) 1090 - .query(&[ 1091 - ("repo", bob_did.as_str()), 1092 - ("collection", "app.bsky.feed.like"), 1093 - ("rkey", like_rkey), 1094 - ]) 1095 - .send() 1096 - .await 1097 - .expect("Failed to check deleted like"); 1098 - 1099 - assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Like should be deleted"); 1100 - } 1101 - 1102 - #[tokio::test] 1103 - async fn test_repost_lifecycle() { 1104 - let client = client(); 1105 - 1106 - let (alice_did, alice_jwt) = setup_new_user("alice-repost").await; 1107 - let (bob_did, bob_jwt) = setup_new_user("bob-repost").await; 1108 - 1109 - let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Repost this!").await; 1110 - 1111 - let (repost_uri, _) = create_repost(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await; 1112 - 1113 - let repost_rkey = repost_uri.split('/').last().unwrap(); 1114 - let get_repost_res = client 1115 - .get(format!( 1116 - "{}/xrpc/com.atproto.repo.getRecord", 1117 - base_url().await 1118 - )) 1119 - .query(&[ 1120 - ("repo", bob_did.as_str()), 1121 - ("collection", "app.bsky.feed.repost"), 1122 - ("rkey", repost_rkey), 1123 - ]) 1124 - .send() 1125 - .await 1126 - .expect("Failed to get repost"); 1127 - 1128 - assert_eq!(get_repost_res.status(), StatusCode::OK); 1129 - let repost_body: Value = get_repost_res.json().await.unwrap(); 1130 - assert_eq!(repost_body["value"]["subject"]["uri"], post_uri); 1131 - 1132 - let delete_payload = json!({ 1133 - "repo": bob_did, 1134 - "collection": "app.bsky.feed.repost", 1135 - "rkey": repost_rkey 1136 - }); 1137 - 1138 - let delete_res = client 1139 - .post(format!( 1140 - "{}/xrpc/com.atproto.repo.deleteRecord", 1141 - base_url().await 1142 - )) 1143 - .bearer_auth(&bob_jwt) 1144 - .json(&delete_payload) 1145 - .send() 1146 - .await 1147 - .expect("Failed to delete repost"); 1148 - 1149 - assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete repost"); 1150 - } 1151 - 1152 - #[tokio::test] 1153 - async fn test_unfollow_lifecycle() { 1154 - let client = client(); 1155 - 1156 - let (alice_did, _alice_jwt) = setup_new_user("alice-unfollow").await; 1157 - let (bob_did, bob_jwt) = setup_new_user("bob-unfollow").await; 1158 - 1159 - let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await; 1160 - 1161 - let follow_rkey = follow_uri.split('/').last().unwrap(); 1162 - let get_follow_res = client 1163 - .get(format!( 1164 - "{}/xrpc/com.atproto.repo.getRecord", 1165 - base_url().await 1166 - )) 1167 - .query(&[ 1168 - ("repo", bob_did.as_str()), 1169 - ("collection", "app.bsky.graph.follow"), 1170 - ("rkey", follow_rkey), 1171 - ]) 1172 - .send() 1173 - .await 1174 - .expect("Failed to get follow"); 1175 - 1176 - assert_eq!(get_follow_res.status(), StatusCode::OK); 1177 - 1178 - let unfollow_payload = json!({ 1179 - "repo": bob_did, 1180 - "collection": "app.bsky.graph.follow", 1181 - "rkey": follow_rkey 1182 - }); 1183 - 1184 - let unfollow_res = client 1185 - .post(format!( 1186 - "{}/xrpc/com.atproto.repo.deleteRecord", 1187 - base_url().await 1188 - )) 1189 - .bearer_auth(&bob_jwt) 1190 - .json(&unfollow_payload) 1191 - .send() 1192 - .await 1193 - .expect("Failed to unfollow"); 1194 - 1195 - assert_eq!(unfollow_res.status(), StatusCode::OK, "Failed to unfollow"); 1196 - 1197 - let get_deleted_res = client 1198 - .get(format!( 1199 - "{}/xrpc/com.atproto.repo.getRecord", 1200 - base_url().await 1201 - )) 1202 - .query(&[ 1203 - ("repo", bob_did.as_str()), 1204 - ("collection", "app.bsky.graph.follow"), 1205 - ("rkey", follow_rkey), 1206 - ]) 1207 - .send() 1208 - .await 1209 - .expect("Failed to check deleted follow"); 1210 - 1211 - assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Follow should be deleted"); 1212 - } 1213 - 1214 - #[tokio::test] 1215 - async fn test_timeline_after_unfollow() { 1216 - let client = client(); 1217 - 1218 - let (alice_did, alice_jwt) = setup_new_user("alice-tl-unfollow").await; 1219 - let (bob_did, bob_jwt) = setup_new_user("bob-tl-unfollow").await; 1220 - 1221 - let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await; 1222 - 1223 - create_post(&client, &alice_did, &alice_jwt, "Post while following").await; 1224 - 1225 - tokio::time::sleep(Duration::from_secs(1)).await; 1226 - 1227 - let timeline_res = client 1228 - .get(format!( 1229 - "{}/xrpc/app.bsky.feed.getTimeline", 1230 - base_url().await 1231 - )) 1232 - .bearer_auth(&bob_jwt) 1233 - .send() 1234 - .await 1235 - .expect("Failed to get timeline"); 1236 - 1237 - assert_eq!(timeline_res.status(), StatusCode::OK); 1238 - let timeline_body: Value = timeline_res.json().await.unwrap(); 1239 - let feed = timeline_body["feed"].as_array().unwrap(); 1240 - assert_eq!(feed.len(), 1, "Should see 1 post from Alice"); 1241 - 1242 - let follow_rkey = follow_uri.split('/').last().unwrap(); 1243 - let unfollow_payload = json!({ 1244 - "repo": bob_did, 1245 - "collection": "app.bsky.graph.follow", 1246 - "rkey": follow_rkey 1247 - }); 1248 - client 1249 - .post(format!( 1250 - "{}/xrpc/com.atproto.repo.deleteRecord", 1251 - base_url().await 1252 - )) 1253 - .bearer_auth(&bob_jwt) 1254 - .json(&unfollow_payload) 1255 - .send() 1256 - .await 1257 - .expect("Failed to unfollow"); 1258 - 1259 - tokio::time::sleep(Duration::from_secs(1)).await; 1260 - 1261 - let timeline_after_res = client 1262 - .get(format!( 1263 - "{}/xrpc/app.bsky.feed.getTimeline", 1264 - base_url().await 1265 - )) 1266 - .bearer_auth(&bob_jwt) 1267 - .send() 1268 - .await 1269 - .expect("Failed to get timeline after unfollow"); 1270 - 1271 - assert_eq!(timeline_after_res.status(), StatusCode::OK); 1272 - let timeline_after: Value = timeline_after_res.json().await.unwrap(); 1273 - let feed_after = timeline_after["feed"].as_array().unwrap(); 1274 - assert_eq!(feed_after.len(), 0, "Should see 0 posts after unfollowing"); 1275 - } 1276 - 1277 - #[tokio::test] 1278 - async fn test_blob_in_record_lifecycle() { 1279 - let client = client(); 1280 - let (did, jwt) = setup_new_user("blob-record").await; 1281 - 1282 - let blob_data = b"This is test blob data for a profile avatar"; 1283 - let upload_res = client 1284 - .post(format!( 1285 - "{}/xrpc/com.atproto.repo.uploadBlob", 1286 - base_url().await 1287 - )) 1288 - .header(header::CONTENT_TYPE, "text/plain") 1289 - .bearer_auth(&jwt) 1290 - .body(blob_data.to_vec()) 1291 - .send() 1292 - .await 1293 - .expect("Failed to upload blob"); 1294 - 1295 - assert_eq!(upload_res.status(), StatusCode::OK); 1296 - let upload_body: Value = upload_res.json().await.unwrap(); 1297 - let blob_ref = upload_body["blob"].clone(); 1298 - 1299 - let profile_payload = json!({ 1300 - "repo": did, 1301 - "collection": "app.bsky.actor.profile", 1302 - "rkey": "self", 1303 - "record": { 1304 - "$type": "app.bsky.actor.profile", 1305 - "displayName": "User With Avatar", 1306 - "avatar": blob_ref 1307 - } 1308 - }); 1309 - 1310 - let create_res = client 1311 - .post(format!( 1312 - "{}/xrpc/com.atproto.repo.putRecord", 1313 - base_url().await 1314 - )) 1315 - .bearer_auth(&jwt) 1316 - .json(&profile_payload) 1317 - .send() 1318 - .await 1319 - .expect("Failed to create profile with blob"); 1320 - 1321 - assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile with blob"); 1322 - 1323 - let get_res = client 1324 - .get(format!( 1325 - "{}/xrpc/com.atproto.repo.getRecord", 1326 - base_url().await 1327 - )) 1328 - .query(&[ 1329 - ("repo", did.as_str()), 1330 - ("collection", "app.bsky.actor.profile"), 1331 - ("rkey", "self"), 1332 - ]) 1333 - .send() 1334 - .await 1335 - .expect("Failed to get profile"); 1336 - 1337 - assert_eq!(get_res.status(), StatusCode::OK); 1338 - let profile: Value = get_res.json().await.unwrap(); 1339 - assert!(profile["value"]["avatar"]["ref"]["$link"].is_string()); 1340 - } 1341 - 1342 - #[tokio::test] 1343 - async fn test_authorization_cannot_modify_other_repo() { 1344 - let client = client(); 1345 - 1346 - let (alice_did, _alice_jwt) = setup_new_user("alice-auth").await; 1347 - let (_bob_did, bob_jwt) = setup_new_user("bob-auth").await; 1348 - 1349 - let post_payload = json!({ 1350 - "repo": alice_did, 1351 - "collection": "app.bsky.feed.post", 1352 - "rkey": "unauthorized-post", 1353 - "record": { 1354 - "$type": "app.bsky.feed.post", 1355 - "text": "Bob trying to post as Alice", 1356 - "createdAt": Utc::now().to_rfc3339() 1357 - } 1358 - }); 1359 - 1360 - let res = client 1361 - .post(format!( 1362 - "{}/xrpc/com.atproto.repo.putRecord", 1363 - base_url().await 1364 - )) 1365 - .bearer_auth(&bob_jwt) 1366 - .json(&post_payload) 1367 - .send() 1368 - .await 1369 - .expect("Failed to send request"); 1370 - 1371 - assert!( 1372 - res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED, 1373 - "Expected 403 or 401 when writing to another user's repo, got {}", 1374 - res.status() 1375 - ); 1376 - } 1377 - 1378 - #[tokio::test] 1379 - async fn test_authorization_cannot_delete_other_record() { 1380 - let client = client(); 1381 - 1382 - let (alice_did, alice_jwt) = setup_new_user("alice-del-auth").await; 1383 - let (_bob_did, bob_jwt) = setup_new_user("bob-del-auth").await; 1384 - 1385 - let (post_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's post").await; 1386 - let post_rkey = post_uri.split('/').last().unwrap(); 1387 - 1388 - let delete_payload = json!({ 1389 - "repo": alice_did, 1390 - "collection": "app.bsky.feed.post", 1391 - "rkey": post_rkey 1392 - }); 1393 - 1394 - let res = client 1395 - .post(format!( 1396 - "{}/xrpc/com.atproto.repo.deleteRecord", 1397 - base_url().await 1398 - )) 1399 - .bearer_auth(&bob_jwt) 1400 - .json(&delete_payload) 1401 - .send() 1402 - .await 1403 - .expect("Failed to send request"); 1404 - 1405 - assert!( 1406 - res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED, 1407 - "Expected 403 or 401 when deleting another user's record, got {}", 1408 - res.status() 1409 - ); 1410 - 1411 - let get_res = client 1412 - .get(format!( 1413 - "{}/xrpc/com.atproto.repo.getRecord", 1414 - base_url().await 1415 - )) 1416 - .query(&[ 1417 - ("repo", alice_did.as_str()), 1418 - ("collection", "app.bsky.feed.post"), 1419 - ("rkey", post_rkey), 1420 - ]) 1421 - .send() 1422 - .await 1423 - .expect("Failed to verify record exists"); 1424 - 1425 - assert_eq!(get_res.status(), StatusCode::OK, "Record should still exist"); 1426 - } 1427 - 1428 - #[tokio::test] 1429 - async fn test_list_records_pagination() { 1430 - let client = client(); 1431 - let (did, jwt) = setup_new_user("list-pagination").await; 1432 - 1433 - for i in 0..5 { 1434 - tokio::time::sleep(Duration::from_millis(50)).await; 1435 - create_post(&client, &did, &jwt, &format!("Post number {}", i)).await; 1436 - } 1437 - 1438 - let list_res = client 1439 - .get(format!( 1440 - "{}/xrpc/com.atproto.repo.listRecords", 1441 - base_url().await 1442 - )) 1443 - .query(&[ 1444 - ("repo", did.as_str()), 1445 - ("collection", "app.bsky.feed.post"), 1446 - ("limit", "2"), 1447 - ]) 1448 - .send() 1449 - .await 1450 - .expect("Failed to list records"); 1451 - 1452 - assert_eq!(list_res.status(), StatusCode::OK); 1453 - let list_body: Value = list_res.json().await.unwrap(); 1454 - let records = list_body["records"].as_array().unwrap(); 1455 - assert_eq!(records.len(), 2, "Should return 2 records with limit=2"); 1456 - 1457 - if let Some(cursor) = list_body["cursor"].as_str() { 1458 - let list_page2_res = client 1459 - .get(format!( 1460 - "{}/xrpc/com.atproto.repo.listRecords", 1461 - base_url().await 1462 - )) 1463 - .query(&[ 1464 - ("repo", did.as_str()), 1465 - ("collection", "app.bsky.feed.post"), 1466 - ("limit", "2"), 1467 - ("cursor", cursor), 1468 - ]) 1469 - .send() 1470 - .await 1471 - .expect("Failed to list records page 2"); 1472 - 1473 - assert_eq!(list_page2_res.status(), StatusCode::OK); 1474 - let page2_body: Value = list_page2_res.json().await.unwrap(); 1475 - let page2_records = page2_body["records"].as_array().unwrap(); 1476 - assert_eq!(page2_records.len(), 2, "Page 2 should have 2 more records"); 1477 - } 1478 - } 1479 - 1480 - #[tokio::test] 1481 - async fn test_mutual_follow_lifecycle() { 1482 - let client = client(); 1483 - 1484 - let (alice_did, alice_jwt) = setup_new_user("alice-mutual").await; 1485 - let (bob_did, bob_jwt) = setup_new_user("bob-mutual").await; 1486 - 1487 - create_follow(&client, &alice_did, &alice_jwt, &bob_did).await; 1488 - create_follow(&client, &bob_did, &bob_jwt, &alice_did).await; 1489 - 1490 - create_post(&client, &alice_did, &alice_jwt, "Alice's post for mutual").await; 1491 - create_post(&client, &bob_did, &bob_jwt, "Bob's post for mutual").await; 1492 - 1493 - tokio::time::sleep(Duration::from_secs(1)).await; 1494 - 1495 - let alice_timeline_res = client 1496 - .get(format!( 1497 - "{}/xrpc/app.bsky.feed.getTimeline", 1498 - base_url().await 1499 - )) 1500 - .bearer_auth(&alice_jwt) 1501 - .send() 1502 - .await 1503 - .expect("Failed to get Alice's timeline"); 1504 - 1505 - assert_eq!(alice_timeline_res.status(), StatusCode::OK); 1506 - let alice_tl: Value = alice_timeline_res.json().await.unwrap(); 1507 - let alice_feed = alice_tl["feed"].as_array().unwrap(); 1508 - assert_eq!(alice_feed.len(), 1, "Alice should see Bob's 1 post"); 1509 - 1510 - let bob_timeline_res = client 1511 - .get(format!( 1512 - "{}/xrpc/app.bsky.feed.getTimeline", 1513 - base_url().await 1514 - )) 1515 - .bearer_auth(&bob_jwt) 1516 - .send() 1517 - .await 1518 - .expect("Failed to get Bob's timeline"); 1519 - 1520 - assert_eq!(bob_timeline_res.status(), StatusCode::OK); 1521 - let bob_tl: Value = bob_timeline_res.json().await.unwrap(); 1522 - let bob_feed = bob_tl["feed"].as_array().unwrap(); 1523 - assert_eq!(bob_feed.len(), 1, "Bob should see Alice's 1 post"); 1524 - } 1525 - 1526 - #[tokio::test] 1527 - async fn test_account_to_post_full_lifecycle() { 1528 - let client = client(); 1529 - let ts = Utc::now().timestamp_millis(); 1530 - let handle = format!("fullcycle-{}.test", ts); 1531 - let email = format!("fullcycle-{}@test.com", ts); 1532 - let password = "fullcycle-password"; 1533 - 1534 - let create_account_res = client 1535 - .post(format!( 1536 - "{}/xrpc/com.atproto.server.createAccount", 1537 - base_url().await 1538 - )) 1539 - .json(&json!({ 1540 - "handle": handle, 1541 - "email": email, 1542 - "password": password 1543 - })) 1544 - .send() 1545 - .await 1546 - .expect("Failed to create account"); 1547 - 1548 - assert_eq!(create_account_res.status(), StatusCode::OK); 1549 - let account_body: Value = create_account_res.json().await.unwrap(); 1550 - let did = account_body["did"].as_str().unwrap().to_string(); 1551 - let access_jwt = account_body["accessJwt"].as_str().unwrap().to_string(); 1552 - 1553 - let get_session_res = client 1554 - .get(format!( 1555 - "{}/xrpc/com.atproto.server.getSession", 1556 - base_url().await 1557 - )) 1558 - .bearer_auth(&access_jwt) 1559 - .send() 1560 - .await 1561 - .expect("Failed to get session"); 1562 - 1563 - assert_eq!(get_session_res.status(), StatusCode::OK); 1564 - let session_body: Value = get_session_res.json().await.unwrap(); 1565 - assert_eq!(session_body["did"], did); 1566 - assert_eq!(session_body["handle"], handle); 1567 - 1568 - let profile_res = client 1569 - .post(format!( 1570 - "{}/xrpc/com.atproto.repo.putRecord", 1571 - base_url().await 1572 - )) 1573 - .bearer_auth(&access_jwt) 1574 - .json(&json!({ 1575 - "repo": did, 1576 - "collection": "app.bsky.actor.profile", 1577 - "rkey": "self", 1578 - "record": { 1579 - "$type": "app.bsky.actor.profile", 1580 - "displayName": "Full Cycle User" 1581 - } 1582 - })) 1583 - .send() 1584 - .await 1585 - .expect("Failed to create profile"); 1586 - 1587 - assert_eq!(profile_res.status(), StatusCode::OK); 1588 - 1589 - let (post_uri, post_cid) = create_post(&client, &did, &access_jwt, "My first post!").await; 1590 - 1591 - let get_post_res = client 1592 - .get(format!( 1593 - "{}/xrpc/com.atproto.repo.getRecord", 1594 - base_url().await 1595 - )) 1596 - .query(&[ 1597 - ("repo", did.as_str()), 1598 - ("collection", "app.bsky.feed.post"), 1599 - ("rkey", post_uri.split('/').last().unwrap()), 1600 - ]) 1601 - .send() 1602 - .await 1603 - .expect("Failed to get post"); 1604 - 1605 - assert_eq!(get_post_res.status(), StatusCode::OK); 1606 - 1607 - create_like(&client, &did, &access_jwt, &post_uri, &post_cid).await; 1608 - 1609 - let describe_res = client 1610 - .get(format!( 1611 - "{}/xrpc/com.atproto.repo.describeRepo", 1612 - base_url().await 1613 - )) 1614 - .query(&[("repo", did.as_str())]) 1615 - .send() 1616 - .await 1617 - .expect("Failed to describe repo"); 1618 - 1619 - assert_eq!(describe_res.status(), StatusCode::OK); 1620 - let describe_body: Value = describe_res.json().await.unwrap(); 1621 - assert_eq!(describe_body["did"], did); 1622 - assert_eq!(describe_body["handle"], handle); 1623 - } 1624 - 1625 - #[tokio::test] 1626 - async fn test_app_password_lifecycle() { 1627 - let client = client(); 1628 - let ts = Utc::now().timestamp_millis(); 1629 - let handle = format!("apppass-{}.test", ts); 1630 - let email = format!("apppass-{}@test.com", ts); 1631 - let password = "apppass-password"; 1632 - 1633 - let create_res = client 1634 - .post(format!( 1635 - "{}/xrpc/com.atproto.server.createAccount", 1636 - base_url().await 1637 - )) 1638 - .json(&json!({ 1639 - "handle": handle, 1640 - "email": email, 1641 - "password": password 1642 - })) 1643 - .send() 1644 - .await 1645 - .expect("Failed to create account"); 1646 - 1647 - assert_eq!(create_res.status(), StatusCode::OK); 1648 - let account: Value = create_res.json().await.unwrap(); 1649 - let jwt = account["accessJwt"].as_str().unwrap(); 1650 - 1651 - let create_app_pass_res = client 1652 - .post(format!( 1653 - "{}/xrpc/com.atproto.server.createAppPassword", 1654 - base_url().await 1655 - )) 1656 - .bearer_auth(jwt) 1657 - .json(&json!({ "name": "Test App" })) 1658 - .send() 1659 - .await 1660 - .expect("Failed to create app password"); 1661 - 1662 - assert_eq!(create_app_pass_res.status(), StatusCode::OK); 1663 - let app_pass: Value = create_app_pass_res.json().await.unwrap(); 1664 - let app_password = app_pass["password"].as_str().unwrap().to_string(); 1665 - assert_eq!(app_pass["name"], "Test App"); 1666 - 1667 - let list_res = client 1668 - .get(format!( 1669 - "{}/xrpc/com.atproto.server.listAppPasswords", 1670 - base_url().await 1671 - )) 1672 - .bearer_auth(jwt) 1673 - .send() 1674 - .await 1675 - .expect("Failed to list app passwords"); 1676 - 1677 - assert_eq!(list_res.status(), StatusCode::OK); 1678 - let list_body: Value = list_res.json().await.unwrap(); 1679 - let passwords = list_body["passwords"].as_array().unwrap(); 1680 - assert_eq!(passwords.len(), 1); 1681 - assert_eq!(passwords[0]["name"], "Test App"); 1682 - 1683 - let login_res = client 1684 - .post(format!( 1685 - "{}/xrpc/com.atproto.server.createSession", 1686 - base_url().await 1687 - )) 1688 - .json(&json!({ 1689 - "identifier": handle, 1690 - "password": app_password 1691 - })) 1692 - .send() 1693 - .await 1694 - .expect("Failed to login with app password"); 1695 - 1696 - assert_eq!(login_res.status(), StatusCode::OK, "App password login should work"); 1697 - 1698 - let revoke_res = client 1699 - .post(format!( 1700 - "{}/xrpc/com.atproto.server.revokeAppPassword", 1701 - base_url().await 1702 - )) 1703 - .bearer_auth(jwt) 1704 - .json(&json!({ "name": "Test App" })) 1705 - .send() 1706 - .await 1707 - .expect("Failed to revoke app password"); 1708 - 1709 - assert_eq!(revoke_res.status(), StatusCode::OK); 1710 - 1711 - let login_after_revoke = client 1712 - .post(format!( 1713 - "{}/xrpc/com.atproto.server.createSession", 1714 - base_url().await 1715 - )) 1716 - .json(&json!({ 1717 - "identifier": handle, 1718 - "password": app_password 1719 - })) 1720 - .send() 1721 - .await 1722 - .expect("Failed to attempt login after revoke"); 1723 - 1724 - assert!( 1725 - login_after_revoke.status() == StatusCode::UNAUTHORIZED 1726 - || login_after_revoke.status() == StatusCode::BAD_REQUEST, 1727 - "Revoked app password should not work" 1728 - ); 1729 - 1730 - let list_after_revoke = client 1731 - .get(format!( 1732 - "{}/xrpc/com.atproto.server.listAppPasswords", 1733 - base_url().await 1734 - )) 1735 - .bearer_auth(jwt) 1736 - .send() 1737 - .await 1738 - .expect("Failed to list after revoke"); 1739 - 1740 - let list_after: Value = list_after_revoke.json().await.unwrap(); 1741 - let passwords_after = list_after["passwords"].as_array().unwrap(); 1742 - assert_eq!(passwords_after.len(), 0, "No app passwords should remain"); 1743 - } 1744 - 1745 - #[tokio::test] 1746 - async fn test_account_deactivation_lifecycle() { 1747 - let client = client(); 1748 - let ts = Utc::now().timestamp_millis(); 1749 - let handle = format!("deactivate-{}.test", ts); 1750 - let email = format!("deactivate-{}@test.com", ts); 1751 - let password = "deactivate-password"; 1752 - 1753 - let create_res = client 1754 - .post(format!( 1755 - "{}/xrpc/com.atproto.server.createAccount", 1756 - base_url().await 1757 - )) 1758 - .json(&json!({ 1759 - "handle": handle, 1760 - "email": email, 1761 - "password": password 1762 - })) 1763 - .send() 1764 - .await 1765 - .expect("Failed to create account"); 1766 - 1767 - assert_eq!(create_res.status(), StatusCode::OK); 1768 - let account: Value = create_res.json().await.unwrap(); 1769 - let did = account["did"].as_str().unwrap().to_string(); 1770 - let jwt = account["accessJwt"].as_str().unwrap().to_string(); 1771 - 1772 - let (post_uri, _) = create_post(&client, &did, &jwt, "Post before deactivation").await; 1773 - let post_rkey = post_uri.split('/').last().unwrap(); 1774 - 1775 - let status_before = client 1776 - .get(format!( 1777 - "{}/xrpc/com.atproto.server.checkAccountStatus", 1778 - base_url().await 1779 - )) 1780 - .bearer_auth(&jwt) 1781 - .send() 1782 - .await 1783 - .expect("Failed to check status"); 1784 - 1785 - assert_eq!(status_before.status(), StatusCode::OK); 1786 - let status_body: Value = status_before.json().await.unwrap(); 1787 - assert_eq!(status_body["activated"], true); 1788 - 1789 - let deactivate_res = client 1790 - .post(format!( 1791 - "{}/xrpc/com.atproto.server.deactivateAccount", 1792 - base_url().await 1793 - )) 1794 - .bearer_auth(&jwt) 1795 - .json(&json!({})) 1796 - .send() 1797 - .await 1798 - .expect("Failed to deactivate"); 1799 - 1800 - assert_eq!(deactivate_res.status(), StatusCode::OK); 1801 - 1802 - let get_post_res = client 1803 - .get(format!( 1804 - "{}/xrpc/com.atproto.repo.getRecord", 1805 - base_url().await 1806 - )) 1807 - .query(&[ 1808 - ("repo", did.as_str()), 1809 - ("collection", "app.bsky.feed.post"), 1810 - ("rkey", post_rkey), 1811 - ]) 1812 - .send() 1813 - .await 1814 - .expect("Failed to get post while deactivated"); 1815 - 1816 - assert_eq!(get_post_res.status(), StatusCode::OK, "Records should still be readable"); 1817 - 1818 - let activate_res = client 1819 - .post(format!( 1820 - "{}/xrpc/com.atproto.server.activateAccount", 1821 - base_url().await 1822 - )) 1823 - .bearer_auth(&jwt) 1824 - .json(&json!({})) 1825 - .send() 1826 - .await 1827 - .expect("Failed to reactivate"); 1828 - 1829 - assert_eq!(activate_res.status(), StatusCode::OK); 1830 - 1831 - let status_after_activate = client 1832 - .get(format!( 1833 - "{}/xrpc/com.atproto.server.checkAccountStatus", 1834 - base_url().await 1835 - )) 1836 - .bearer_auth(&jwt) 1837 - .send() 1838 - .await 1839 - .expect("Failed to check status after activate"); 1840 - 1841 - assert_eq!(status_after_activate.status(), StatusCode::OK); 1842 - 1843 - let (new_post_uri, _) = create_post(&client, &did, &jwt, "Post after reactivation").await; 1844 - assert!(!new_post_uri.is_empty(), "Should be able to post after reactivation"); 1845 - } 1846 - 1847 - #[tokio::test] 1848 - async fn test_sync_record_lifecycle() { 1849 - let client = client(); 1850 - let (did, jwt) = setup_new_user("sync-record-lifecycle").await; 1851 - 1852 - let (post_uri, _post_cid) = 1853 - create_post(&client, &did, &jwt, "Post for sync record test").await; 1854 - let post_rkey = post_uri.split('/').last().unwrap(); 1855 - 1856 - let sync_record_res = client 1857 - .get(format!( 1858 - "{}/xrpc/com.atproto.sync.getRecord", 1859 - base_url().await 1860 - )) 1861 - .query(&[ 1862 - ("did", did.as_str()), 1863 - ("collection", "app.bsky.feed.post"), 1864 - ("rkey", post_rkey), 1865 - ]) 1866 - .send() 1867 - .await 1868 - .expect("Failed to get sync record"); 1869 - 1870 - assert_eq!(sync_record_res.status(), StatusCode::OK); 1871 - assert_eq!( 1872 - sync_record_res 1873 - .headers() 1874 - .get("content-type") 1875 - .and_then(|h| h.to_str().ok()), 1876 - Some("application/vnd.ipld.car") 1877 - ); 1878 - let car_bytes = sync_record_res.bytes().await.unwrap(); 1879 - assert!(!car_bytes.is_empty(), "CAR data should not be empty"); 1880 - 1881 - let latest_before = client 1882 - .get(format!( 1883 - "{}/xrpc/com.atproto.sync.getLatestCommit", 1884 - base_url().await 1885 - )) 1886 - .query(&[("did", did.as_str())]) 1887 - .send() 1888 - .await 1889 - .expect("Failed to get latest commit"); 1890 - let latest_before_body: Value = latest_before.json().await.unwrap(); 1891 - let rev_before = latest_before_body["rev"].as_str().unwrap().to_string(); 1892 - 1893 - let (post2_uri, _) = create_post(&client, &did, &jwt, "Second post for sync test").await; 1894 - 1895 - let latest_after = client 1896 - .get(format!( 1897 - "{}/xrpc/com.atproto.sync.getLatestCommit", 1898 - base_url().await 1899 - )) 1900 - .query(&[("did", did.as_str())]) 1901 - .send() 1902 - .await 1903 - .expect("Failed to get latest commit after"); 1904 - let latest_after_body: Value = latest_after.json().await.unwrap(); 1905 - let rev_after = latest_after_body["rev"].as_str().unwrap().to_string(); 1906 - assert_ne!(rev_before, rev_after, "Revision should change after new record"); 1907 - 1908 - let delete_payload = json!({ 1909 - "repo": did, 1910 - "collection": "app.bsky.feed.post", 1911 - "rkey": post_rkey 1912 - }); 1913 - let delete_res = client 1914 - .post(format!( 1915 - "{}/xrpc/com.atproto.repo.deleteRecord", 1916 - base_url().await 1917 - )) 1918 - .bearer_auth(&jwt) 1919 - .json(&delete_payload) 1920 - .send() 1921 - .await 1922 - .expect("Failed to delete record"); 1923 - assert_eq!(delete_res.status(), StatusCode::OK); 1924 - 1925 - let sync_deleted_res = client 1926 - .get(format!( 1927 - "{}/xrpc/com.atproto.sync.getRecord", 1928 - base_url().await 1929 - )) 1930 - .query(&[ 1931 - ("did", did.as_str()), 1932 - ("collection", "app.bsky.feed.post"), 1933 - ("rkey", post_rkey), 1934 - ]) 1935 - .send() 1936 - .await 1937 - .expect("Failed to check deleted record via sync"); 1938 - assert_eq!( 1939 - sync_deleted_res.status(), 1940 - StatusCode::NOT_FOUND, 1941 - "Deleted record should return 404 via sync.getRecord" 1942 - ); 1943 - 1944 - let post2_rkey = post2_uri.split('/').last().unwrap(); 1945 - let sync_post2_res = client 1946 - .get(format!( 1947 - "{}/xrpc/com.atproto.sync.getRecord", 1948 - base_url().await 1949 - )) 1950 - .query(&[ 1951 - ("did", did.as_str()), 1952 - ("collection", "app.bsky.feed.post"), 1953 - ("rkey", post2_rkey), 1954 - ]) 1955 - .send() 1956 - .await 1957 - .expect("Failed to get second post via sync"); 1958 - assert_eq!( 1959 - sync_post2_res.status(), 1960 - StatusCode::OK, 1961 - "Second post should still be accessible" 1962 - ); 1963 - } 1964 - 1965 - #[tokio::test] 1966 - async fn test_sync_repo_export_lifecycle() { 1967 - let client = client(); 1968 - let (did, jwt) = setup_new_user("sync-repo-export").await; 1969 - 1970 - let profile_payload = json!({ 1971 - "repo": did, 1972 - "collection": "app.bsky.actor.profile", 1973 - "rkey": "self", 1974 - "record": { 1975 - "$type": "app.bsky.actor.profile", 1976 - "displayName": "Sync Export User" 1977 - } 1978 - }); 1979 - let profile_res = client 1980 - .post(format!( 1981 - "{}/xrpc/com.atproto.repo.putRecord", 1982 - base_url().await 1983 - )) 1984 - .bearer_auth(&jwt) 1985 - .json(&profile_payload) 1986 - .send() 1987 - .await 1988 - .expect("Failed to create profile"); 1989 - assert_eq!(profile_res.status(), StatusCode::OK); 1990 - 1991 - for i in 0..3 { 1992 - tokio::time::sleep(Duration::from_millis(50)).await; 1993 - create_post(&client, &did, &jwt, &format!("Export test post {}", i)).await; 1994 - } 1995 - 1996 - let blob_data = b"blob data for sync export test"; 1997 - let upload_res = client 1998 - .post(format!( 1999 - "{}/xrpc/com.atproto.repo.uploadBlob", 2000 - base_url().await 2001 - )) 2002 - .header(header::CONTENT_TYPE, "application/octet-stream") 2003 - .bearer_auth(&jwt) 2004 - .body(blob_data.to_vec()) 2005 - .send() 2006 - .await 2007 - .expect("Failed to upload blob"); 2008 - assert_eq!(upload_res.status(), StatusCode::OK); 2009 - let blob_body: Value = upload_res.json().await.unwrap(); 2010 - let blob_cid = blob_body["blob"]["ref"]["$link"].as_str().unwrap().to_string(); 2011 - 2012 - let repo_status_res = client 2013 - .get(format!( 2014 - "{}/xrpc/com.atproto.sync.getRepoStatus", 2015 - base_url().await 2016 - )) 2017 - .query(&[("did", did.as_str())]) 2018 - .send() 2019 - .await 2020 - .expect("Failed to get repo status"); 2021 - assert_eq!(repo_status_res.status(), StatusCode::OK); 2022 - let status_body: Value = repo_status_res.json().await.unwrap(); 2023 - assert_eq!(status_body["did"], did); 2024 - assert_eq!(status_body["active"], true); 2025 - 2026 - let get_repo_res = client 2027 - .get(format!( 2028 - "{}/xrpc/com.atproto.sync.getRepo", 2029 - base_url().await 2030 - )) 2031 - .query(&[("did", did.as_str())]) 2032 - .send() 2033 - .await 2034 - .expect("Failed to get full repo"); 2035 - assert_eq!(get_repo_res.status(), StatusCode::OK); 2036 - assert_eq!( 2037 - get_repo_res 2038 - .headers() 2039 - .get("content-type") 2040 - .and_then(|h| h.to_str().ok()), 2041 - Some("application/vnd.ipld.car") 2042 - ); 2043 - let repo_car = get_repo_res.bytes().await.unwrap(); 2044 - assert!(repo_car.len() > 100, "Repo CAR should have substantial data"); 2045 - 2046 - let list_blobs_res = client 2047 - .get(format!( 2048 - "{}/xrpc/com.atproto.sync.listBlobs", 2049 - base_url().await 2050 - )) 2051 - .query(&[("did", did.as_str())]) 2052 - .send() 2053 - .await 2054 - .expect("Failed to list blobs"); 2055 - assert_eq!(list_blobs_res.status(), StatusCode::OK); 2056 - let blobs_body: Value = list_blobs_res.json().await.unwrap(); 2057 - let cids = blobs_body["cids"].as_array().unwrap(); 2058 - assert!(!cids.is_empty(), "Should have at least one blob"); 2059 - 2060 - let get_blob_res = client 2061 - .get(format!( 2062 - "{}/xrpc/com.atproto.sync.getBlob", 2063 - base_url().await 2064 - )) 2065 - .query(&[("did", did.as_str()), ("cid", &blob_cid)]) 2066 - .send() 2067 - .await 2068 - .expect("Failed to get blob"); 2069 - assert_eq!(get_blob_res.status(), StatusCode::OK); 2070 - let retrieved_blob = get_blob_res.bytes().await.unwrap(); 2071 - assert_eq!( 2072 - retrieved_blob.as_ref(), 2073 - blob_data, 2074 - "Retrieved blob should match uploaded data" 2075 - ); 2076 - 2077 - let latest_commit_res = client 2078 - .get(format!( 2079 - "{}/xrpc/com.atproto.sync.getLatestCommit", 2080 - base_url().await 2081 - )) 2082 - .query(&[("did", did.as_str())]) 2083 - .send() 2084 - .await 2085 - .expect("Failed to get latest commit"); 2086 - assert_eq!(latest_commit_res.status(), StatusCode::OK); 2087 - let commit_body: Value = latest_commit_res.json().await.unwrap(); 2088 - let root_cid = commit_body["cid"].as_str().unwrap(); 2089 - 2090 - let get_blocks_url = format!( 2091 - "{}/xrpc/com.atproto.sync.getBlocks?did={}&cids={}", 2092 - base_url().await, 2093 - did, 2094 - root_cid 2095 - ); 2096 - let get_blocks_res = client 2097 - .get(&get_blocks_url) 2098 - .send() 2099 - .await 2100 - .expect("Failed to get blocks"); 2101 - assert_eq!(get_blocks_res.status(), StatusCode::OK); 2102 - assert_eq!( 2103 - get_blocks_res 2104 - .headers() 2105 - .get("content-type") 2106 - .and_then(|h| h.to_str().ok()), 2107 - Some("application/vnd.ipld.car") 2108 - ); 2109 - } 2110 - 2111 - #[tokio::test] 2112 - async fn test_apply_writes_batch_lifecycle() { 2113 - let client = client(); 2114 - let (did, jwt) = setup_new_user("apply-writes-batch").await; 2115 - 2116 - let now = Utc::now().to_rfc3339(); 2117 - let writes_payload = json!({ 2118 - "repo": did, 2119 - "writes": [ 2120 - { 2121 - "$type": "com.atproto.repo.applyWrites#create", 2122 - "collection": "app.bsky.feed.post", 2123 - "rkey": "batch-post-1", 2124 - "value": { 2125 - "$type": "app.bsky.feed.post", 2126 - "text": "First batch post", 2127 - "createdAt": now 2128 - } 2129 - }, 2130 - { 2131 - "$type": "com.atproto.repo.applyWrites#create", 2132 - "collection": "app.bsky.feed.post", 2133 - "rkey": "batch-post-2", 2134 - "value": { 2135 - "$type": "app.bsky.feed.post", 2136 - "text": "Second batch post", 2137 - "createdAt": now 2138 - } 2139 - }, 2140 - { 2141 - "$type": "com.atproto.repo.applyWrites#create", 2142 - "collection": "app.bsky.actor.profile", 2143 - "rkey": "self", 2144 - "value": { 2145 - "$type": "app.bsky.actor.profile", 2146 - "displayName": "Batch User" 2147 - } 2148 - } 2149 - ] 2150 - }); 2151 - 2152 - let apply_res = client 2153 - .post(format!( 2154 - "{}/xrpc/com.atproto.repo.applyWrites", 2155 - base_url().await 2156 - )) 2157 - .bearer_auth(&jwt) 2158 - .json(&writes_payload) 2159 - .send() 2160 - .await 2161 - .expect("Failed to apply writes"); 2162 - 2163 - assert_eq!(apply_res.status(), StatusCode::OK); 2164 - 2165 - let get_post1 = client 2166 - .get(format!( 2167 - "{}/xrpc/com.atproto.repo.getRecord", 2168 - base_url().await 2169 - )) 2170 - .query(&[ 2171 - ("repo", did.as_str()), 2172 - ("collection", "app.bsky.feed.post"), 2173 - ("rkey", "batch-post-1"), 2174 - ]) 2175 - .send() 2176 - .await 2177 - .expect("Failed to get post 1"); 2178 - assert_eq!(get_post1.status(), StatusCode::OK); 2179 - let post1_body: Value = get_post1.json().await.unwrap(); 2180 - assert_eq!(post1_body["value"]["text"], "First batch post"); 2181 - 2182 - let get_post2 = client 2183 - .get(format!( 2184 - "{}/xrpc/com.atproto.repo.getRecord", 2185 - base_url().await 2186 - )) 2187 - .query(&[ 2188 - ("repo", did.as_str()), 2189 - ("collection", "app.bsky.feed.post"), 2190 - ("rkey", "batch-post-2"), 2191 - ]) 2192 - .send() 2193 - .await 2194 - .expect("Failed to get post 2"); 2195 - assert_eq!(get_post2.status(), StatusCode::OK); 2196 - 2197 - let get_profile = client 2198 - .get(format!( 2199 - "{}/xrpc/com.atproto.repo.getRecord", 2200 - base_url().await 2201 - )) 2202 - .query(&[ 2203 - ("repo", did.as_str()), 2204 - ("collection", "app.bsky.actor.profile"), 2205 - ("rkey", "self"), 2206 - ]) 2207 - .send() 2208 - .await 2209 - .expect("Failed to get profile"); 2210 - assert_eq!(get_profile.status(), StatusCode::OK); 2211 - let profile_body: Value = get_profile.json().await.unwrap(); 2212 - assert_eq!(profile_body["value"]["displayName"], "Batch User"); 2213 - 2214 - let update_writes = json!({ 2215 - "repo": did, 2216 - "writes": [ 2217 - { 2218 - "$type": "com.atproto.repo.applyWrites#update", 2219 - "collection": "app.bsky.actor.profile", 2220 - "rkey": "self", 2221 - "value": { 2222 - "$type": "app.bsky.actor.profile", 2223 - "displayName": "Updated Batch User" 2224 - } 2225 - }, 2226 - { 2227 - "$type": "com.atproto.repo.applyWrites#delete", 2228 - "collection": "app.bsky.feed.post", 2229 - "rkey": "batch-post-1" 2230 - } 2231 - ] 2232 - }); 2233 - 2234 - let update_res = client 2235 - .post(format!( 2236 - "{}/xrpc/com.atproto.repo.applyWrites", 2237 - base_url().await 2238 - )) 2239 - .bearer_auth(&jwt) 2240 - .json(&update_writes) 2241 - .send() 2242 - .await 2243 - .expect("Failed to apply update writes"); 2244 - assert_eq!(update_res.status(), StatusCode::OK); 2245 - 2246 - let get_updated_profile = client 2247 - .get(format!( 2248 - "{}/xrpc/com.atproto.repo.getRecord", 2249 - base_url().await 2250 - )) 2251 - .query(&[ 2252 - ("repo", did.as_str()), 2253 - ("collection", "app.bsky.actor.profile"), 2254 - ("rkey", "self"), 2255 - ]) 2256 - .send() 2257 - .await 2258 - .expect("Failed to get updated profile"); 2259 - let updated_profile: Value = get_updated_profile.json().await.unwrap(); 2260 - assert_eq!(updated_profile["value"]["displayName"], "Updated Batch User"); 2261 - 2262 - let get_deleted_post = client 2263 - .get(format!( 2264 - "{}/xrpc/com.atproto.repo.getRecord", 2265 - base_url().await 2266 - )) 2267 - .query(&[ 2268 - ("repo", did.as_str()), 2269 - ("collection", "app.bsky.feed.post"), 2270 - ("rkey", "batch-post-1"), 2271 - ]) 2272 - .send() 2273 - .await 2274 - .expect("Failed to check deleted post"); 2275 - assert_eq!( 2276 - get_deleted_post.status(), 2277 - StatusCode::NOT_FOUND, 2278 - "Batch-deleted post should be gone" 2279 - ); 2280 - } 2281 - 2282 - #[tokio::test] 2283 - async fn test_resolve_handle_lifecycle() { 2284 - let client = client(); 2285 - let ts = Utc::now().timestamp_millis(); 2286 - let handle = format!("resolve-test-{}.test", ts); 2287 - let email = format!("resolve-test-{}@test.com", ts); 2288 - 2289 - let create_res = client 2290 - .post(format!( 2291 - "{}/xrpc/com.atproto.server.createAccount", 2292 - base_url().await 2293 - )) 2294 - .json(&json!({ 2295 - "handle": handle, 2296 - "email": email, 2297 - "password": "resolve-test-pw" 2298 - })) 2299 - .send() 2300 - .await 2301 - .expect("Failed to create account"); 2302 - assert_eq!(create_res.status(), StatusCode::OK); 2303 - let account: Value = create_res.json().await.unwrap(); 2304 - let did = account["did"].as_str().unwrap(); 2305 - 2306 - let resolve_res = client 2307 - .get(format!( 2308 - "{}/xrpc/com.atproto.identity.resolveHandle", 2309 - base_url().await 2310 - )) 2311 - .query(&[("handle", handle.as_str())]) 2312 - .send() 2313 - .await 2314 - .expect("Failed to resolve handle"); 2315 - 2316 - assert_eq!(resolve_res.status(), StatusCode::OK); 2317 - let resolve_body: Value = resolve_res.json().await.unwrap(); 2318 - assert_eq!(resolve_body["did"], did); 2319 - } 2320 - 2321 - #[tokio::test] 2322 - async fn test_service_auth_lifecycle() { 2323 - let client = client(); 2324 - let (did, jwt) = setup_new_user("service-auth-test").await; 2325 - 2326 - let service_auth_res = client 2327 - .get(format!( 2328 - "{}/xrpc/com.atproto.server.getServiceAuth", 2329 - base_url().await 2330 - )) 2331 - .query(&[ 2332 - ("aud", "did:web:api.bsky.app"), 2333 - ("lxm", "com.atproto.repo.uploadBlob"), 2334 - ]) 2335 - .bearer_auth(&jwt) 2336 - .send() 2337 - .await 2338 - .expect("Failed to get service auth"); 2339 - 2340 - assert_eq!(service_auth_res.status(), StatusCode::OK); 2341 - let auth_body: Value = service_auth_res.json().await.unwrap(); 2342 - let service_token = auth_body["token"].as_str().expect("No token in response"); 2343 - 2344 - let parts: Vec<&str> = service_token.split('.').collect(); 2345 - assert_eq!(parts.len(), 3, "Service token should be a valid JWT"); 2346 - 2347 - let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD 2348 - .decode(parts[1]) 2349 - .expect("Failed to decode JWT payload"); 2350 - let claims: Value = serde_json::from_slice(&payload_bytes).expect("Invalid JWT payload"); 2351 - 2352 - assert_eq!(claims["iss"], did); 2353 - assert_eq!(claims["aud"], "did:web:api.bsky.app"); 2354 - assert_eq!(claims["lxm"], "com.atproto.repo.uploadBlob"); 2355 - } 2356 - 2357 - #[tokio::test] 2358 - async fn test_moderation_report_lifecycle() { 2359 - let client = client(); 2360 - let (alice_did, alice_jwt) = setup_new_user("alice-report").await; 2361 - let (bob_did, bob_jwt) = setup_new_user("bob-report").await; 2362 - 2363 - let (post_uri, post_cid) = 2364 - create_post(&client, &bob_did, &bob_jwt, "This is a reportable post").await; 2365 - 2366 - let report_payload = json!({ 2367 - "reasonType": "com.atproto.moderation.defs#reasonSpam", 2368 - "reason": "This looks like spam to me", 2369 - "subject": { 2370 - "$type": "com.atproto.repo.strongRef", 2371 - "uri": post_uri, 2372 - "cid": post_cid 2373 - } 2374 - }); 2375 - 2376 - let report_res = client 2377 - .post(format!( 2378 - "{}/xrpc/com.atproto.moderation.createReport", 2379 - base_url().await 2380 - )) 2381 - .bearer_auth(&alice_jwt) 2382 - .json(&report_payload) 2383 - .send() 2384 - .await 2385 - .expect("Failed to create report"); 2386 - 2387 - assert_eq!(report_res.status(), StatusCode::OK); 2388 - let report_body: Value = report_res.json().await.unwrap(); 2389 - assert!(report_body["id"].is_number(), "Report should have an ID"); 2390 - assert_eq!(report_body["reasonType"], "com.atproto.moderation.defs#reasonSpam"); 2391 - assert_eq!(report_body["reportedBy"], alice_did); 2392 - 2393 - let account_report_payload = json!({ 2394 - "reasonType": "com.atproto.moderation.defs#reasonOther", 2395 - "reason": "Suspicious account activity", 2396 - "subject": { 2397 - "$type": "com.atproto.admin.defs#repoRef", 2398 - "did": bob_did 2399 - } 2400 - }); 2401 - 2402 - let account_report_res = client 2403 - .post(format!( 2404 - "{}/xrpc/com.atproto.moderation.createReport", 2405 - base_url().await 2406 - )) 2407 - .bearer_auth(&alice_jwt) 2408 - .json(&account_report_payload) 2409 - .send() 2410 - .await 2411 - .expect("Failed to create account report"); 2412 - 2413 - assert_eq!(account_report_res.status(), StatusCode::OK); 2414 - }
···
+139
tests/lifecycle_session.rs
··· 304 let passwords_after = list_after["passwords"].as_array().unwrap(); 305 assert_eq!(passwords_after.len(), 0, "No app passwords should remain"); 306 }
··· 304 let passwords_after = list_after["passwords"].as_array().unwrap(); 305 assert_eq!(passwords_after.len(), 0, "No app passwords should remain"); 306 } 307 + 308 + #[tokio::test] 309 + async fn test_account_deactivation_lifecycle() { 310 + let client = client(); 311 + let ts = Utc::now().timestamp_millis(); 312 + let handle = format!("deactivate-{}.test", ts); 313 + let email = format!("deactivate-{}@test.com", ts); 314 + let password = "deactivate-password"; 315 + 316 + let create_res = client 317 + .post(format!( 318 + "{}/xrpc/com.atproto.server.createAccount", 319 + base_url().await 320 + )) 321 + .json(&json!({ 322 + "handle": handle, 323 + "email": email, 324 + "password": password 325 + })) 326 + .send() 327 + .await 328 + .expect("Failed to create account"); 329 + 330 + assert_eq!(create_res.status(), StatusCode::OK); 331 + let account: Value = create_res.json().await.unwrap(); 332 + let did = account["did"].as_str().unwrap().to_string(); 333 + let jwt = account["accessJwt"].as_str().unwrap().to_string(); 334 + 335 + let (post_uri, _) = create_post(&client, &did, &jwt, "Post before deactivation").await; 336 + let post_rkey = post_uri.split('/').last().unwrap(); 337 + 338 + let status_before = client 339 + .get(format!( 340 + "{}/xrpc/com.atproto.server.checkAccountStatus", 341 + base_url().await 342 + )) 343 + .bearer_auth(&jwt) 344 + .send() 345 + .await 346 + .expect("Failed to check status"); 347 + 348 + assert_eq!(status_before.status(), StatusCode::OK); 349 + let status_body: Value = status_before.json().await.unwrap(); 350 + assert_eq!(status_body["activated"], true); 351 + 352 + let deactivate_res = client 353 + .post(format!( 354 + "{}/xrpc/com.atproto.server.deactivateAccount", 355 + base_url().await 356 + )) 357 + .bearer_auth(&jwt) 358 + .json(&json!({})) 359 + .send() 360 + .await 361 + .expect("Failed to deactivate"); 362 + 363 + assert_eq!(deactivate_res.status(), StatusCode::OK); 364 + 365 + let get_post_res = client 366 + .get(format!( 367 + "{}/xrpc/com.atproto.repo.getRecord", 368 + base_url().await 369 + )) 370 + .query(&[ 371 + ("repo", did.as_str()), 372 + ("collection", "app.bsky.feed.post"), 373 + ("rkey", post_rkey), 374 + ]) 375 + .send() 376 + .await 377 + .expect("Failed to get post while deactivated"); 378 + 379 + assert_eq!(get_post_res.status(), StatusCode::OK, "Records should still be readable"); 380 + 381 + let activate_res = client 382 + .post(format!( 383 + "{}/xrpc/com.atproto.server.activateAccount", 384 + base_url().await 385 + )) 386 + .bearer_auth(&jwt) 387 + .json(&json!({})) 388 + .send() 389 + .await 390 + .expect("Failed to reactivate"); 391 + 392 + assert_eq!(activate_res.status(), StatusCode::OK); 393 + 394 + let status_after_activate = client 395 + .get(format!( 396 + "{}/xrpc/com.atproto.server.checkAccountStatus", 397 + base_url().await 398 + )) 399 + .bearer_auth(&jwt) 400 + .send() 401 + .await 402 + .expect("Failed to check status after activate"); 403 + 404 + assert_eq!(status_after_activate.status(), StatusCode::OK); 405 + 406 + let (new_post_uri, _) = create_post(&client, &did, &jwt, "Post after reactivation").await; 407 + assert!(!new_post_uri.is_empty(), "Should be able to post after reactivation"); 408 + } 409 + 410 + #[tokio::test] 411 + async fn test_service_auth_lifecycle() { 412 + let client = client(); 413 + let (did, jwt) = setup_new_user("service-auth-test").await; 414 + 415 + let service_auth_res = client 416 + .get(format!( 417 + "{}/xrpc/com.atproto.server.getServiceAuth", 418 + base_url().await 419 + )) 420 + .query(&[ 421 + ("aud", "did:web:api.bsky.app"), 422 + ("lxm", "com.atproto.repo.uploadBlob"), 423 + ]) 424 + .bearer_auth(&jwt) 425 + .send() 426 + .await 427 + .expect("Failed to get service auth"); 428 + 429 + assert_eq!(service_auth_res.status(), StatusCode::OK); 430 + let auth_body: Value = service_auth_res.json().await.unwrap(); 431 + let service_token = auth_body["token"].as_str().expect("No token in response"); 432 + 433 + let parts: Vec<&str> = service_token.split('.').collect(); 434 + assert_eq!(parts.len(), 3, "Service token should be a valid JWT"); 435 + 436 + use base64::Engine; 437 + let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD 438 + .decode(parts[1]) 439 + .expect("Failed to decode JWT payload"); 440 + let claims: Value = serde_json::from_slice(&payload_bytes).expect("Invalid JWT payload"); 441 + 442 + assert_eq!(claims["iss"], did); 443 + assert_eq!(claims["aud"], "did:web:api.bsky.app"); 444 + assert_eq!(claims["lxm"], "com.atproto.repo.uploadBlob"); 445 + }
+100
tests/lifecycle_social.rs
··· 7 use reqwest::StatusCode; 8 use serde_json::{Value, json}; 9 use std::time::Duration; 10 11 #[tokio::test] 12 async fn test_social_flow_lifecycle() { ··· 414 let bob_feed = bob_tl["feed"].as_array().unwrap(); 415 assert_eq!(bob_feed.len(), 1, "Bob should see Alice's 1 post"); 416 }
··· 7 use reqwest::StatusCode; 8 use serde_json::{Value, json}; 9 use std::time::Duration; 10 + use chrono::Utc; 11 12 #[tokio::test] 13 async fn test_social_flow_lifecycle() { ··· 415 let bob_feed = bob_tl["feed"].as_array().unwrap(); 416 assert_eq!(bob_feed.len(), 1, "Bob should see Alice's 1 post"); 417 } 418 + 419 + #[tokio::test] 420 + async fn test_account_to_post_full_lifecycle() { 421 + let client = client(); 422 + let ts = Utc::now().timestamp_millis(); 423 + let handle = format!("fullcycle-{}.test", ts); 424 + let email = format!("fullcycle-{}@test.com", ts); 425 + let password = "fullcycle-password"; 426 + 427 + let create_account_res = client 428 + .post(format!( 429 + "{}/xrpc/com.atproto.server.createAccount", 430 + base_url().await 431 + )) 432 + .json(&json!({ 433 + "handle": handle, 434 + "email": email, 435 + "password": password 436 + })) 437 + .send() 438 + .await 439 + .expect("Failed to create account"); 440 + 441 + assert_eq!(create_account_res.status(), StatusCode::OK); 442 + let account_body: Value = create_account_res.json().await.unwrap(); 443 + let did = account_body["did"].as_str().unwrap().to_string(); 444 + let access_jwt = account_body["accessJwt"].as_str().unwrap().to_string(); 445 + 446 + let get_session_res = client 447 + .get(format!( 448 + "{}/xrpc/com.atproto.server.getSession", 449 + base_url().await 450 + )) 451 + .bearer_auth(&access_jwt) 452 + .send() 453 + .await 454 + .expect("Failed to get session"); 455 + 456 + assert_eq!(get_session_res.status(), StatusCode::OK); 457 + let session_body: Value = get_session_res.json().await.unwrap(); 458 + assert_eq!(session_body["did"], did); 459 + assert_eq!(session_body["handle"], handle); 460 + 461 + let profile_res = client 462 + .post(format!( 463 + "{}/xrpc/com.atproto.repo.putRecord", 464 + base_url().await 465 + )) 466 + .bearer_auth(&access_jwt) 467 + .json(&json!({ 468 + "repo": did, 469 + "collection": "app.bsky.actor.profile", 470 + "rkey": "self", 471 + "record": { 472 + "$type": "app.bsky.actor.profile", 473 + "displayName": "Full Cycle User" 474 + } 475 + })) 476 + .send() 477 + .await 478 + .expect("Failed to create profile"); 479 + 480 + assert_eq!(profile_res.status(), StatusCode::OK); 481 + 482 + let (post_uri, post_cid) = create_post(&client, &did, &access_jwt, "My first post!").await; 483 + 484 + let get_post_res = client 485 + .get(format!( 486 + "{}/xrpc/com.atproto.repo.getRecord", 487 + base_url().await 488 + )) 489 + .query(&[ 490 + ("repo", did.as_str()), 491 + ("collection", "app.bsky.feed.post"), 492 + ("rkey", post_uri.split('/').last().unwrap()), 493 + ]) 494 + .send() 495 + .await 496 + .expect("Failed to get post"); 497 + 498 + assert_eq!(get_post_res.status(), StatusCode::OK); 499 + 500 + create_like(&client, &did, &access_jwt, &post_uri, &post_cid).await; 501 + 502 + let describe_res = client 503 + .get(format!( 504 + "{}/xrpc/com.atproto.repo.describeRepo", 505 + base_url().await 506 + )) 507 + .query(&[("repo", did.as_str())]) 508 + .send() 509 + .await 510 + .expect("Failed to describe repo"); 511 + 512 + assert_eq!(describe_res.status(), StatusCode::OK); 513 + let describe_body: Value = describe_res.json().await.unwrap(); 514 + assert_eq!(describe_body["did"], did); 515 + assert_eq!(describe_body["handle"], handle); 516 + }
+67
tests/moderation.rs
···
··· 1 + mod common; 2 + mod helpers; 3 + 4 + use common::*; 5 + use helpers::*; 6 + 7 + use reqwest::StatusCode; 8 + use serde_json::{Value, json}; 9 + 10 + #[tokio::test] 11 + async fn test_moderation_report_lifecycle() { 12 + let client = client(); 13 + let (alice_did, alice_jwt) = setup_new_user("alice-report").await; 14 + let (bob_did, bob_jwt) = setup_new_user("bob-report").await; 15 + 16 + let (post_uri, post_cid) = 17 + create_post(&client, &bob_did, &bob_jwt, "This is a reportable post").await; 18 + 19 + let report_payload = json!({ 20 + "reasonType": "com.atproto.moderation.defs#reasonSpam", 21 + "reason": "This looks like spam to me", 22 + "subject": { 23 + "$type": "com.atproto.repo.strongRef", 24 + "uri": post_uri, 25 + "cid": post_cid 26 + } 27 + }); 28 + 29 + let report_res = client 30 + .post(format!( 31 + "{}/xrpc/com.atproto.moderation.createReport", 32 + base_url().await 33 + )) 34 + .bearer_auth(&alice_jwt) 35 + .json(&report_payload) 36 + .send() 37 + .await 38 + .expect("Failed to create report"); 39 + 40 + assert_eq!(report_res.status(), StatusCode::OK); 41 + let report_body: Value = report_res.json().await.unwrap(); 42 + assert!(report_body["id"].is_number(), "Report should have an ID"); 43 + assert_eq!(report_body["reasonType"], "com.atproto.moderation.defs#reasonSpam"); 44 + assert_eq!(report_body["reportedBy"], alice_did); 45 + 46 + let account_report_payload = json!({ 47 + "reasonType": "com.atproto.moderation.defs#reasonOther", 48 + "reason": "Suspicious account activity", 49 + "subject": { 50 + "$type": "com.atproto.admin.defs#repoRef", 51 + "did": bob_did 52 + } 53 + }); 54 + 55 + let account_report_res = client 56 + .post(format!( 57 + "{}/xrpc/com.atproto.moderation.createReport", 58 + base_url().await 59 + )) 60 + .bearer_auth(&alice_jwt) 61 + .json(&account_report_payload) 62 + .send() 63 + .await 64 + .expect("Failed to create account report"); 65 + 66 + assert_eq!(account_report_res.status(), StatusCode::OK); 67 + }
+270 -1
tests/sync_repo.rs
··· 1 mod common; 2 use common::*; 3 use reqwest::StatusCode; 4 - use serde_json::Value; 5 6 #[tokio::test] 7 async fn test_get_latest_commit_success() { ··· 429 430 assert_eq!(res.status(), StatusCode::NOT_FOUND); 431 }
··· 1 mod common; 2 + mod helpers; 3 use common::*; 4 + use helpers::*; 5 + 6 use reqwest::StatusCode; 7 + use reqwest::header; 8 + use serde_json::{Value, json}; 9 + use chrono::Utc; 10 11 #[tokio::test] 12 async fn test_get_latest_commit_success() { ··· 434 435 assert_eq!(res.status(), StatusCode::NOT_FOUND); 436 } 437 + 438 + #[tokio::test] 439 + async fn test_sync_record_lifecycle() { 440 + let client = client(); 441 + let (did, jwt) = setup_new_user("sync-record-lifecycle").await; 442 + 443 + let (post_uri, _post_cid) = 444 + create_post(&client, &did, &jwt, "Post for sync record test").await; 445 + let post_rkey = post_uri.split('/').last().unwrap(); 446 + 447 + let sync_record_res = client 448 + .get(format!( 449 + "{}/xrpc/com.atproto.sync.getRecord", 450 + base_url().await 451 + )) 452 + .query(&[ 453 + ("did", did.as_str()), 454 + ("collection", "app.bsky.feed.post"), 455 + ("rkey", post_rkey), 456 + ]) 457 + .send() 458 + .await 459 + .expect("Failed to get sync record"); 460 + 461 + assert_eq!(sync_record_res.status(), StatusCode::OK); 462 + assert_eq!( 463 + sync_record_res 464 + .headers() 465 + .get("content-type") 466 + .and_then(|h| h.to_str().ok()), 467 + Some("application/vnd.ipld.car") 468 + ); 469 + let car_bytes = sync_record_res.bytes().await.unwrap(); 470 + assert!(!car_bytes.is_empty(), "CAR data should not be empty"); 471 + 472 + let latest_before = client 473 + .get(format!( 474 + "{}/xrpc/com.atproto.sync.getLatestCommit", 475 + base_url().await 476 + )) 477 + .query(&[("did", did.as_str())]) 478 + .send() 479 + .await 480 + .expect("Failed to get latest commit"); 481 + let latest_before_body: Value = latest_before.json().await.unwrap(); 482 + let rev_before = latest_before_body["rev"].as_str().unwrap().to_string(); 483 + 484 + let (post2_uri, _) = create_post(&client, &did, &jwt, "Second post for sync test").await; 485 + 486 + let latest_after = client 487 + .get(format!( 488 + "{}/xrpc/com.atproto.sync.getLatestCommit", 489 + base_url().await 490 + )) 491 + .query(&[("did", did.as_str())]) 492 + .send() 493 + .await 494 + .expect("Failed to get latest commit after"); 495 + let latest_after_body: Value = latest_after.json().await.unwrap(); 496 + let rev_after = latest_after_body["rev"].as_str().unwrap().to_string(); 497 + assert_ne!(rev_before, rev_after, "Revision should change after new record"); 498 + 499 + let delete_payload = json!({ 500 + "repo": did, 501 + "collection": "app.bsky.feed.post", 502 + "rkey": post_rkey 503 + }); 504 + let delete_res = client 505 + .post(format!( 506 + "{}/xrpc/com.atproto.repo.deleteRecord", 507 + base_url().await 508 + )) 509 + .bearer_auth(&jwt) 510 + .json(&delete_payload) 511 + .send() 512 + .await 513 + .expect("Failed to delete record"); 514 + assert_eq!(delete_res.status(), StatusCode::OK); 515 + 516 + let sync_deleted_res = client 517 + .get(format!( 518 + "{}/xrpc/com.atproto.sync.getRecord", 519 + base_url().await 520 + )) 521 + .query(&[ 522 + ("did", did.as_str()), 523 + ("collection", "app.bsky.feed.post"), 524 + ("rkey", post_rkey), 525 + ]) 526 + .send() 527 + .await 528 + .expect("Failed to check deleted record via sync"); 529 + assert_eq!( 530 + sync_deleted_res.status(), 531 + StatusCode::NOT_FOUND, 532 + "Deleted record should return 404 via sync.getRecord" 533 + ); 534 + 535 + let post2_rkey = post2_uri.split('/').last().unwrap(); 536 + let sync_post2_res = client 537 + .get(format!( 538 + "{}/xrpc/com.atproto.sync.getRecord", 539 + base_url().await 540 + )) 541 + .query(&[ 542 + ("did", did.as_str()), 543 + ("collection", "app.bsky.feed.post"), 544 + ("rkey", post2_rkey), 545 + ]) 546 + .send() 547 + .await 548 + .expect("Failed to get second post via sync"); 549 + assert_eq!( 550 + sync_post2_res.status(), 551 + StatusCode::OK, 552 + "Second post should still be accessible" 553 + ); 554 + } 555 + 556 + #[tokio::test] 557 + async fn test_sync_repo_export_lifecycle() { 558 + let client = client(); 559 + let (did, jwt) = setup_new_user("sync-repo-export").await; 560 + 561 + let profile_payload = json!({ 562 + "repo": did, 563 + "collection": "app.bsky.actor.profile", 564 + "rkey": "self", 565 + "record": { 566 + "$type": "app.bsky.actor.profile", 567 + "displayName": "Sync Export User" 568 + } 569 + }); 570 + let profile_res = client 571 + .post(format!( 572 + "{}/xrpc/com.atproto.repo.putRecord", 573 + base_url().await 574 + )) 575 + .bearer_auth(&jwt) 576 + .json(&profile_payload) 577 + .send() 578 + .await 579 + .expect("Failed to create profile"); 580 + assert_eq!(profile_res.status(), StatusCode::OK); 581 + 582 + for i in 0..3 { 583 + tokio::time::sleep(std::time::Duration::from_millis(50)).await; 584 + create_post(&client, &did, &jwt, &format!("Export test post {}", i)).await; 585 + } 586 + 587 + let blob_data = b"blob data for sync export test"; 588 + let upload_res = client 589 + .post(format!( 590 + "{}/xrpc/com.atproto.repo.uploadBlob", 591 + base_url().await 592 + )) 593 + .header(header::CONTENT_TYPE, "application/octet-stream") 594 + .bearer_auth(&jwt) 595 + .body(blob_data.to_vec()) 596 + .send() 597 + .await 598 + .expect("Failed to upload blob"); 599 + assert_eq!(upload_res.status(), StatusCode::OK); 600 + let blob_body: Value = upload_res.json().await.unwrap(); 601 + let blob_cid = blob_body["blob"]["ref"]["$link"].as_str().unwrap().to_string(); 602 + 603 + let repo_status_res = client 604 + .get(format!( 605 + "{}/xrpc/com.atproto.sync.getRepoStatus", 606 + base_url().await 607 + )) 608 + .query(&[("did", did.as_str())]) 609 + .send() 610 + .await 611 + .expect("Failed to get repo status"); 612 + assert_eq!(repo_status_res.status(), StatusCode::OK); 613 + let status_body: Value = repo_status_res.json().await.unwrap(); 614 + assert_eq!(status_body["did"], did); 615 + assert_eq!(status_body["active"], true); 616 + 617 + let get_repo_res = client 618 + .get(format!( 619 + "{}/xrpc/com.atproto.sync.getRepo", 620 + base_url().await 621 + )) 622 + .query(&[("did", did.as_str())]) 623 + .send() 624 + .await 625 + .expect("Failed to get full repo"); 626 + assert_eq!(get_repo_res.status(), StatusCode::OK); 627 + assert_eq!( 628 + get_repo_res 629 + .headers() 630 + .get("content-type") 631 + .and_then(|h| h.to_str().ok()), 632 + Some("application/vnd.ipld.car") 633 + ); 634 + let repo_car = get_repo_res.bytes().await.unwrap(); 635 + assert!(repo_car.len() > 100, "Repo CAR should have substantial data"); 636 + 637 + let list_blobs_res = client 638 + .get(format!( 639 + "{}/xrpc/com.atproto.sync.listBlobs", 640 + base_url().await 641 + )) 642 + .query(&[("did", did.as_str())]) 643 + .send() 644 + .await 645 + .expect("Failed to list blobs"); 646 + assert_eq!(list_blobs_res.status(), StatusCode::OK); 647 + let blobs_body: Value = list_blobs_res.json().await.unwrap(); 648 + let cids = blobs_body["cids"].as_array().unwrap(); 649 + assert!(!cids.is_empty(), "Should have at least one blob"); 650 + 651 + let get_blob_res = client 652 + .get(format!( 653 + "{}/xrpc/com.atproto.sync.getBlob", 654 + base_url().await 655 + )) 656 + .query(&[("did", did.as_str()), ("cid", &blob_cid)]) 657 + .send() 658 + .await 659 + .expect("Failed to get blob"); 660 + assert_eq!(get_blob_res.status(), StatusCode::OK); 661 + let retrieved_blob = get_blob_res.bytes().await.unwrap(); 662 + assert_eq!( 663 + retrieved_blob.as_ref(), 664 + blob_data, 665 + "Retrieved blob should match uploaded data" 666 + ); 667 + 668 + let latest_commit_res = client 669 + .get(format!( 670 + "{}/xrpc/com.atproto.sync.getLatestCommit", 671 + base_url().await 672 + )) 673 + .query(&[("did", did.as_str())]) 674 + .send() 675 + .await 676 + .expect("Failed to get latest commit"); 677 + assert_eq!(latest_commit_res.status(), StatusCode::OK); 678 + let commit_body: Value = latest_commit_res.json().await.unwrap(); 679 + let root_cid = commit_body["cid"].as_str().unwrap(); 680 + 681 + let get_blocks_url = format!( 682 + "{}/xrpc/com.atproto.sync.getBlocks?did={}&cids={}", 683 + base_url().await, 684 + did, 685 + root_cid 686 + ); 687 + let get_blocks_res = client 688 + .get(&get_blocks_url) 689 + .send() 690 + .await 691 + .expect("Failed to get blocks"); 692 + assert_eq!(get_blocks_res.status(), StatusCode::OK); 693 + assert_eq!( 694 + get_blocks_res 695 + .headers() 696 + .get("content-type") 697 + .and_then(|h| h.to_str().ok()), 698 + Some("application/vnd.ipld.car") 699 + ); 700 + }