this repo has no description
1mod common;
2use common::*;
3
4use chrono::Utc;
5use reqwest;
6use serde_json::{Value, json};
7use std::time::Duration;
8
9async fn setup_new_user(handle_prefix: &str) -> (String, String) {
10 let client = client();
11 let ts = Utc::now().timestamp_millis();
12 let handle = format!("{}-{}.test", handle_prefix, ts);
13 let email = format!("{}-{}@test.com", handle_prefix, ts);
14 let password = "e2e-password-123";
15
16 let create_account_payload = json!({
17 "handle": handle,
18 "email": email,
19 "password": password
20 });
21 let create_res = client
22 .post(format!(
23 "{}/xrpc/com.atproto.server.createAccount",
24 base_url().await
25 ))
26 .json(&create_account_payload)
27 .send()
28 .await
29 .expect("setup_new_user: Failed to send createAccount");
30
31 if create_res.status() != reqwest::StatusCode::OK {
32 panic!(
33 "setup_new_user: Failed to create account: {:?}",
34 create_res.text().await
35 );
36 }
37
38 let create_body: Value = create_res
39 .json()
40 .await
41 .expect("setup_new_user: createAccount response was not JSON");
42
43 let new_did = create_body["did"]
44 .as_str()
45 .expect("setup_new_user: Response had no DID")
46 .to_string();
47 let new_jwt = create_body["accessJwt"]
48 .as_str()
49 .expect("setup_new_user: Response had no accessJwt")
50 .to_string();
51
52 (new_did, new_jwt)
53}
54
55#[tokio::test]
56#[ignore]
57async 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(¶ms)
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(¶ms)
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(¶ms)
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#[ignore]
225async fn test_record_update_conflict_lifecycle() {
226 let client = client();
227 let (user_did, user_jwt) = setup_new_user("user-conflict").await;
228
229 let profile_payload = json!({
230 "repo": user_did,
231 "collection": "app.bsky.actor.profile",
232 "rkey": "self",
233 "record": {
234 "$type": "app.bsky.actor.profile",
235 "displayName": "Original Name"
236 }
237 });
238 let create_res = client
239 .post(format!(
240 "{}/xrpc/com.atproto.repo.putRecord",
241 base_url().await
242 ))
243 .bearer_auth(&user_jwt)
244 .json(&profile_payload)
245 .send()
246 .await
247 .expect("create profile failed");
248
249 if create_res.status() != reqwest::StatusCode::OK {
250 return;
251 }
252
253 let get_res = client
254 .get(format!(
255 "{}/xrpc/com.atproto.repo.getRecord",
256 base_url().await
257 ))
258 .query(&[
259 ("repo", &user_did),
260 ("collection", &"app.bsky.actor.profile".to_string()),
261 ("rkey", &"self".to_string()),
262 ])
263 .send()
264 .await
265 .expect("getRecord failed");
266 let get_body: Value = get_res.json().await.expect("getRecord not json");
267 let cid_v1 = get_body["cid"]
268 .as_str()
269 .expect("Profile v1 had no CID")
270 .to_string();
271
272 let update_payload_v2 = json!({
273 "repo": user_did,
274 "collection": "app.bsky.actor.profile",
275 "rkey": "self",
276 "record": {
277 "$type": "app.bsky.actor.profile",
278 "displayName": "Updated Name (v2)"
279 },
280 "swapCommit": cid_v1 // <-- Correctly point to v1
281 });
282 let update_res_v2 = client
283 .post(format!(
284 "{}/xrpc/com.atproto.repo.putRecord",
285 base_url().await
286 ))
287 .bearer_auth(&user_jwt)
288 .json(&update_payload_v2)
289 .send()
290 .await
291 .expect("putRecord v2 failed");
292 assert_eq!(
293 update_res_v2.status(),
294 reqwest::StatusCode::OK,
295 "v2 update failed"
296 );
297 let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json");
298 let cid_v2 = update_body_v2["cid"]
299 .as_str()
300 .expect("v2 response had no CID")
301 .to_string();
302
303 let update_payload_v3_stale = json!({
304 "repo": user_did,
305 "collection": "app.bsky.actor.profile",
306 "rkey": "self",
307 "record": {
308 "$type": "app.bsky.actor.profile",
309 "displayName": "Stale Update (v3)"
310 },
311 "swapCommit": cid_v1
312 });
313 let update_res_v3_stale = client
314 .post(format!(
315 "{}/xrpc/com.atproto.repo.putRecord",
316 base_url().await
317 ))
318 .bearer_auth(&user_jwt)
319 .json(&update_payload_v3_stale)
320 .send()
321 .await
322 .expect("putRecord v3 (stale) failed");
323
324 assert_eq!(
325 update_res_v3_stale.status(),
326 reqwest::StatusCode::CONFLICT,
327 "Stale update did not cause a 409 Conflict"
328 );
329
330 let update_payload_v3_good = json!({
331 "repo": user_did,
332 "collection": "app.bsky.actor.profile",
333 "rkey": "self",
334 "record": {
335 "$type": "app.bsky.actor.profile",
336 "displayName": "Good Update (v3)"
337 },
338 "swapCommit": cid_v2 // <-- Correct
339 });
340 let update_res_v3_good = client
341 .post(format!(
342 "{}/xrpc/com.atproto.repo.putRecord",
343 base_url().await
344 ))
345 .bearer_auth(&user_jwt)
346 .json(&update_payload_v3_good)
347 .send()
348 .await
349 .expect("putRecord v3 (good) failed");
350
351 assert_eq!(
352 update_res_v3_good.status(),
353 reqwest::StatusCode::OK,
354 "v3 (good) update failed"
355 );
356}
357
358async fn create_post(
359 client: &reqwest::Client,
360 did: &str,
361 jwt: &str,
362 text: &str,
363) -> (String, String) {
364 let collection = "app.bsky.feed.post";
365 let rkey = format!("e2e_social_{}", Utc::now().timestamp_millis());
366 let now = Utc::now().to_rfc3339();
367
368 let create_payload = json!({
369 "repo": did,
370 "collection": collection,
371 "rkey": rkey,
372 "record": {
373 "$type": collection,
374 "text": text,
375 "createdAt": now
376 }
377 });
378
379 let create_res = client
380 .post(format!(
381 "{}/xrpc/com.atproto.repo.putRecord",
382 base_url().await
383 ))
384 .bearer_auth(jwt)
385 .json(&create_payload)
386 .send()
387 .await
388 .expect("Failed to send create post request");
389
390 assert_eq!(
391 create_res.status(),
392 reqwest::StatusCode::OK,
393 "Failed to create post record"
394 );
395 let create_body: Value = create_res
396 .json()
397 .await
398 .expect("create post response was not JSON");
399 let uri = create_body["uri"].as_str().unwrap().to_string();
400 let cid = create_body["cid"].as_str().unwrap().to_string();
401 (uri, cid)
402}
403
404async fn create_follow(
405 client: &reqwest::Client,
406 follower_did: &str,
407 follower_jwt: &str,
408 followee_did: &str,
409) -> (String, String) {
410 let collection = "app.bsky.graph.follow";
411 let rkey = format!("e2e_follow_{}", Utc::now().timestamp_millis());
412 let now = Utc::now().to_rfc3339();
413
414 let create_payload = json!({
415 "repo": follower_did,
416 "collection": collection,
417 "rkey": rkey,
418 "record": {
419 "$type": collection,
420 "subject": followee_did,
421 "createdAt": now
422 }
423 });
424
425 let create_res = client
426 .post(format!(
427 "{}/xrpc/com.atproto.repo.putRecord",
428 base_url().await
429 ))
430 .bearer_auth(follower_jwt)
431 .json(&create_payload)
432 .send()
433 .await
434 .expect("Failed to send create follow request");
435
436 assert_eq!(
437 create_res.status(),
438 reqwest::StatusCode::OK,
439 "Failed to create follow record"
440 );
441 let create_body: Value = create_res
442 .json()
443 .await
444 .expect("create follow response was not JSON");
445 let uri = create_body["uri"].as_str().unwrap().to_string();
446 let cid = create_body["cid"].as_str().unwrap().to_string();
447 (uri, cid)
448}
449
450#[tokio::test]
451#[ignore]
452async fn test_social_flow_lifecycle() {
453 let client = client();
454
455 let (alice_did, alice_jwt) = setup_new_user("alice-social").await;
456 let (bob_did, bob_jwt) = setup_new_user("bob-social").await;
457
458 let (post1_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's first post!").await;
459
460 create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
461
462 tokio::time::sleep(Duration::from_secs(1)).await;
463
464 let timeline_res_1 = client
465 .get(format!(
466 "{}/xrpc/app.bsky.feed.getTimeline",
467 base_url().await
468 ))
469 .bearer_auth(&bob_jwt)
470 .send()
471 .await
472 .expect("Failed to get timeline (1)");
473
474 assert_eq!(
475 timeline_res_1.status(),
476 reqwest::StatusCode::OK,
477 "Failed to get timeline (1)"
478 );
479 let timeline_body_1: Value = timeline_res_1.json().await.expect("Timeline (1) not JSON");
480 let feed_1 = timeline_body_1["feed"].as_array().unwrap();
481 assert_eq!(feed_1.len(), 1, "Timeline should have 1 post");
482 assert_eq!(
483 feed_1[0]["post"]["uri"], post1_uri,
484 "Post URI mismatch in timeline (1)"
485 );
486
487 let (post2_uri, _) = create_post(
488 &client,
489 &alice_did,
490 &alice_jwt,
491 "Alice's second post, so exciting!",
492 )
493 .await;
494
495 tokio::time::sleep(Duration::from_secs(1)).await;
496
497 let timeline_res_2 = client
498 .get(format!(
499 "{}/xrpc/app.bsky.feed.getTimeline",
500 base_url().await
501 ))
502 .bearer_auth(&bob_jwt)
503 .send()
504 .await
505 .expect("Failed to get timeline (2)");
506
507 assert_eq!(
508 timeline_res_2.status(),
509 reqwest::StatusCode::OK,
510 "Failed to get timeline (2)"
511 );
512 let timeline_body_2: Value = timeline_res_2.json().await.expect("Timeline (2) not JSON");
513 let feed_2 = timeline_body_2["feed"].as_array().unwrap();
514 assert_eq!(feed_2.len(), 2, "Timeline should have 2 posts");
515 assert_eq!(
516 feed_2[0]["post"]["uri"], post2_uri,
517 "Post 2 should be first"
518 );
519 assert_eq!(
520 feed_2[1]["post"]["uri"], post1_uri,
521 "Post 1 should be second"
522 );
523
524 let delete_payload = json!({
525 "repo": alice_did,
526 "collection": "app.bsky.feed.post",
527 "rkey": post1_uri.split('/').last().unwrap()
528 });
529 let delete_res = client
530 .post(format!(
531 "{}/xrpc/com.atproto.repo.deleteRecord",
532 base_url().await
533 ))
534 .bearer_auth(&alice_jwt)
535 .json(&delete_payload)
536 .send()
537 .await
538 .expect("Failed to send delete request");
539 assert_eq!(
540 delete_res.status(),
541 reqwest::StatusCode::OK,
542 "Failed to delete record"
543 );
544
545 tokio::time::sleep(Duration::from_secs(1)).await;
546
547 let timeline_res_3 = client
548 .get(format!(
549 "{}/xrpc/app.bsky.feed.getTimeline",
550 base_url().await
551 ))
552 .bearer_auth(&bob_jwt)
553 .send()
554 .await
555 .expect("Failed to get timeline (3)");
556
557 assert_eq!(
558 timeline_res_3.status(),
559 reqwest::StatusCode::OK,
560 "Failed to get timeline (3)"
561 );
562 let timeline_body_3: Value = timeline_res_3.json().await.expect("Timeline (3) not JSON");
563 let feed_3 = timeline_body_3["feed"].as_array().unwrap();
564 assert_eq!(feed_3.len(), 1, "Timeline should have 1 post after delete");
565 assert_eq!(
566 feed_3[0]["post"]["uri"], post2_uri,
567 "Only post 2 should remain"
568 );
569}