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]
56async fn test_post_crud_lifecycle() {
57 let client = client();
58 let (did, jwt) = setup_new_user("lifecycle-crud").await;
59 let collection = "app.bsky.feed.post";
60
61 let rkey = format!("e2e_lifecycle_{}", Utc::now().timestamp_millis());
62 let now = Utc::now().to_rfc3339();
63
64 let original_text = "Hello from the lifecycle test!";
65 let create_payload = json!({
66 "repo": did,
67 "collection": collection,
68 "rkey": rkey,
69 "record": {
70 "$type": collection,
71 "text": original_text,
72 "createdAt": now
73 }
74 });
75
76 let create_res = client
77 .post(format!(
78 "{}/xrpc/com.atproto.repo.putRecord",
79 base_url().await
80 ))
81 .bearer_auth(&jwt)
82 .json(&create_payload)
83 .send()
84 .await
85 .expect("Failed to send create request");
86
87 if create_res.status() != reqwest::StatusCode::OK {
88 let status = create_res.status();
89 let body = create_res
90 .text()
91 .await
92 .unwrap_or_else(|_| "Could not get body".to_string());
93 panic!(
94 "Failed to create record. Status: {}, Body: {}",
95 status, body
96 );
97 }
98
99 let create_body: Value = create_res
100 .json()
101 .await
102 .expect("create response was not JSON");
103 let uri = create_body["uri"].as_str().unwrap();
104
105 let params = [
106 ("repo", did.as_str()),
107 ("collection", collection),
108 ("rkey", &rkey),
109 ];
110 let get_res = client
111 .get(format!(
112 "{}/xrpc/com.atproto.repo.getRecord",
113 base_url().await
114 ))
115 .query(¶ms)
116 .send()
117 .await
118 .expect("Failed to send get request");
119
120 assert_eq!(
121 get_res.status(),
122 reqwest::StatusCode::OK,
123 "Failed to get record after create"
124 );
125 let get_body: Value = get_res.json().await.expect("get response was not JSON");
126 assert_eq!(get_body["uri"], uri);
127 assert_eq!(get_body["value"]["text"], original_text);
128
129 let updated_text = "This post has been updated.";
130 let update_payload = json!({
131 "repo": did,
132 "collection": collection,
133 "rkey": rkey,
134 "record": {
135 "$type": collection,
136 "text": updated_text,
137 "createdAt": now
138 }
139 });
140
141 let update_res = client
142 .post(format!(
143 "{}/xrpc/com.atproto.repo.putRecord",
144 base_url().await
145 ))
146 .bearer_auth(&jwt)
147 .json(&update_payload)
148 .send()
149 .await
150 .expect("Failed to send update request");
151
152 assert_eq!(
153 update_res.status(),
154 reqwest::StatusCode::OK,
155 "Failed to update record"
156 );
157
158 let get_updated_res = client
159 .get(format!(
160 "{}/xrpc/com.atproto.repo.getRecord",
161 base_url().await
162 ))
163 .query(¶ms)
164 .send()
165 .await
166 .expect("Failed to send get-after-update request");
167
168 assert_eq!(
169 get_updated_res.status(),
170 reqwest::StatusCode::OK,
171 "Failed to get record after update"
172 );
173 let get_updated_body: Value = get_updated_res
174 .json()
175 .await
176 .expect("get-updated response was not JSON");
177 assert_eq!(
178 get_updated_body["value"]["text"], updated_text,
179 "Text was not updated"
180 );
181
182 let delete_payload = json!({
183 "repo": did,
184 "collection": collection,
185 "rkey": rkey
186 });
187
188 let delete_res = client
189 .post(format!(
190 "{}/xrpc/com.atproto.repo.deleteRecord",
191 base_url().await
192 ))
193 .bearer_auth(&jwt)
194 .json(&delete_payload)
195 .send()
196 .await
197 .expect("Failed to send delete request");
198
199 assert_eq!(
200 delete_res.status(),
201 reqwest::StatusCode::OK,
202 "Failed to delete record"
203 );
204
205 let get_deleted_res = client
206 .get(format!(
207 "{}/xrpc/com.atproto.repo.getRecord",
208 base_url().await
209 ))
210 .query(¶ms)
211 .send()
212 .await
213 .expect("Failed to send get-after-delete request");
214
215 assert_eq!(
216 get_deleted_res.status(),
217 reqwest::StatusCode::NOT_FOUND,
218 "Record was found, but it should be deleted"
219 );
220}
221
222#[tokio::test]
223async fn test_record_update_conflict_lifecycle() {
224 let client = client();
225 let (user_did, user_jwt) = setup_new_user("user-conflict").await;
226
227 let profile_payload = json!({
228 "repo": user_did,
229 "collection": "app.bsky.actor.profile",
230 "rkey": "self",
231 "record": {
232 "$type": "app.bsky.actor.profile",
233 "displayName": "Original Name"
234 }
235 });
236 let create_res = client
237 .post(format!(
238 "{}/xrpc/com.atproto.repo.putRecord",
239 base_url().await
240 ))
241 .bearer_auth(&user_jwt)
242 .json(&profile_payload)
243 .send()
244 .await
245 .expect("create profile failed");
246
247 if create_res.status() != reqwest::StatusCode::OK {
248 return;
249 }
250
251 let get_res = client
252 .get(format!(
253 "{}/xrpc/com.atproto.repo.getRecord",
254 base_url().await
255 ))
256 .query(&[
257 ("repo", &user_did),
258 ("collection", &"app.bsky.actor.profile".to_string()),
259 ("rkey", &"self".to_string()),
260 ])
261 .send()
262 .await
263 .expect("getRecord failed");
264 let get_body: Value = get_res.json().await.expect("getRecord not json");
265 let cid_v1 = get_body["cid"]
266 .as_str()
267 .expect("Profile v1 had no CID")
268 .to_string();
269
270 let update_payload_v2 = json!({
271 "repo": user_did,
272 "collection": "app.bsky.actor.profile",
273 "rkey": "self",
274 "record": {
275 "$type": "app.bsky.actor.profile",
276 "displayName": "Updated Name (v2)"
277 },
278 "swapRecord": cid_v1
279 });
280 let update_res_v2 = client
281 .post(format!(
282 "{}/xrpc/com.atproto.repo.putRecord",
283 base_url().await
284 ))
285 .bearer_auth(&user_jwt)
286 .json(&update_payload_v2)
287 .send()
288 .await
289 .expect("putRecord v2 failed");
290 assert_eq!(
291 update_res_v2.status(),
292 reqwest::StatusCode::OK,
293 "v2 update failed"
294 );
295 let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json");
296 let cid_v2 = update_body_v2["cid"]
297 .as_str()
298 .expect("v2 response had no CID")
299 .to_string();
300
301 let update_payload_v3_stale = json!({
302 "repo": user_did,
303 "collection": "app.bsky.actor.profile",
304 "rkey": "self",
305 "record": {
306 "$type": "app.bsky.actor.profile",
307 "displayName": "Stale Update (v3)"
308 },
309 "swapRecord": cid_v1
310 });
311 let update_res_v3_stale = client
312 .post(format!(
313 "{}/xrpc/com.atproto.repo.putRecord",
314 base_url().await
315 ))
316 .bearer_auth(&user_jwt)
317 .json(&update_payload_v3_stale)
318 .send()
319 .await
320 .expect("putRecord v3 (stale) failed");
321
322 assert_eq!(
323 update_res_v3_stale.status(),
324 reqwest::StatusCode::CONFLICT,
325 "Stale update did not cause a 409 Conflict"
326 );
327
328 let update_payload_v3_good = json!({
329 "repo": user_did,
330 "collection": "app.bsky.actor.profile",
331 "rkey": "self",
332 "record": {
333 "$type": "app.bsky.actor.profile",
334 "displayName": "Good Update (v3)"
335 },
336 "swapRecord": cid_v2
337 });
338 let update_res_v3_good = client
339 .post(format!(
340 "{}/xrpc/com.atproto.repo.putRecord",
341 base_url().await
342 ))
343 .bearer_auth(&user_jwt)
344 .json(&update_payload_v3_good)
345 .send()
346 .await
347 .expect("putRecord v3 (good) failed");
348
349 assert_eq!(
350 update_res_v3_good.status(),
351 reqwest::StatusCode::OK,
352 "v3 (good) update failed"
353 );
354}
355
356async fn create_post(
357 client: &reqwest::Client,
358 did: &str,
359 jwt: &str,
360 text: &str,
361) -> (String, String) {
362 let collection = "app.bsky.feed.post";
363 let rkey = format!("e2e_social_{}", Utc::now().timestamp_millis());
364 let now = Utc::now().to_rfc3339();
365
366 let create_payload = json!({
367 "repo": did,
368 "collection": collection,
369 "rkey": rkey,
370 "record": {
371 "$type": collection,
372 "text": text,
373 "createdAt": now
374 }
375 });
376
377 let create_res = client
378 .post(format!(
379 "{}/xrpc/com.atproto.repo.putRecord",
380 base_url().await
381 ))
382 .bearer_auth(jwt)
383 .json(&create_payload)
384 .send()
385 .await
386 .expect("Failed to send create post request");
387
388 assert_eq!(
389 create_res.status(),
390 reqwest::StatusCode::OK,
391 "Failed to create post record"
392 );
393 let create_body: Value = create_res
394 .json()
395 .await
396 .expect("create post response was not JSON");
397 let uri = create_body["uri"].as_str().unwrap().to_string();
398 let cid = create_body["cid"].as_str().unwrap().to_string();
399 (uri, cid)
400}
401
402async fn create_follow(
403 client: &reqwest::Client,
404 follower_did: &str,
405 follower_jwt: &str,
406 followee_did: &str,
407) -> (String, String) {
408 let collection = "app.bsky.graph.follow";
409 let rkey = format!("e2e_follow_{}", Utc::now().timestamp_millis());
410 let now = Utc::now().to_rfc3339();
411
412 let create_payload = json!({
413 "repo": follower_did,
414 "collection": collection,
415 "rkey": rkey,
416 "record": {
417 "$type": collection,
418 "subject": followee_did,
419 "createdAt": now
420 }
421 });
422
423 let create_res = client
424 .post(format!(
425 "{}/xrpc/com.atproto.repo.putRecord",
426 base_url().await
427 ))
428 .bearer_auth(follower_jwt)
429 .json(&create_payload)
430 .send()
431 .await
432 .expect("Failed to send create follow request");
433
434 assert_eq!(
435 create_res.status(),
436 reqwest::StatusCode::OK,
437 "Failed to create follow record"
438 );
439 let create_body: Value = create_res
440 .json()
441 .await
442 .expect("create follow response was not JSON");
443 let uri = create_body["uri"].as_str().unwrap().to_string();
444 let cid = create_body["cid"].as_str().unwrap().to_string();
445 (uri, cid)
446}
447
448#[tokio::test]
449async fn test_social_flow_lifecycle() {
450 let client = client();
451
452 let (alice_did, alice_jwt) = setup_new_user("alice-social").await;
453 let (bob_did, bob_jwt) = setup_new_user("bob-social").await;
454
455 let (post1_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's first post!").await;
456
457 create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
458
459 tokio::time::sleep(Duration::from_secs(1)).await;
460
461 let timeline_res_1 = client
462 .get(format!(
463 "{}/xrpc/app.bsky.feed.getTimeline",
464 base_url().await
465 ))
466 .bearer_auth(&bob_jwt)
467 .send()
468 .await
469 .expect("Failed to get timeline (1)");
470
471 assert_eq!(
472 timeline_res_1.status(),
473 reqwest::StatusCode::OK,
474 "Failed to get timeline (1)"
475 );
476 let timeline_body_1: Value = timeline_res_1.json().await.expect("Timeline (1) not JSON");
477 let feed_1 = timeline_body_1["feed"].as_array().unwrap();
478 assert_eq!(feed_1.len(), 1, "Timeline should have 1 post");
479 assert_eq!(
480 feed_1[0]["post"]["uri"], post1_uri,
481 "Post URI mismatch in timeline (1)"
482 );
483
484 let (post2_uri, _) = create_post(
485 &client,
486 &alice_did,
487 &alice_jwt,
488 "Alice's second post, so exciting!",
489 )
490 .await;
491
492 tokio::time::sleep(Duration::from_secs(1)).await;
493
494 let timeline_res_2 = client
495 .get(format!(
496 "{}/xrpc/app.bsky.feed.getTimeline",
497 base_url().await
498 ))
499 .bearer_auth(&bob_jwt)
500 .send()
501 .await
502 .expect("Failed to get timeline (2)");
503
504 assert_eq!(
505 timeline_res_2.status(),
506 reqwest::StatusCode::OK,
507 "Failed to get timeline (2)"
508 );
509 let timeline_body_2: Value = timeline_res_2.json().await.expect("Timeline (2) not JSON");
510 let feed_2 = timeline_body_2["feed"].as_array().unwrap();
511 assert_eq!(feed_2.len(), 2, "Timeline should have 2 posts");
512 assert_eq!(
513 feed_2[0]["post"]["uri"], post2_uri,
514 "Post 2 should be first"
515 );
516 assert_eq!(
517 feed_2[1]["post"]["uri"], post1_uri,
518 "Post 1 should be second"
519 );
520
521 let delete_payload = json!({
522 "repo": alice_did,
523 "collection": "app.bsky.feed.post",
524 "rkey": post1_uri.split('/').last().unwrap()
525 });
526 let delete_res = client
527 .post(format!(
528 "{}/xrpc/com.atproto.repo.deleteRecord",
529 base_url().await
530 ))
531 .bearer_auth(&alice_jwt)
532 .json(&delete_payload)
533 .send()
534 .await
535 .expect("Failed to send delete request");
536 assert_eq!(
537 delete_res.status(),
538 reqwest::StatusCode::OK,
539 "Failed to delete record"
540 );
541
542 tokio::time::sleep(Duration::from_secs(1)).await;
543
544 let timeline_res_3 = client
545 .get(format!(
546 "{}/xrpc/app.bsky.feed.getTimeline",
547 base_url().await
548 ))
549 .bearer_auth(&bob_jwt)
550 .send()
551 .await
552 .expect("Failed to get timeline (3)");
553
554 assert_eq!(
555 timeline_res_3.status(),
556 reqwest::StatusCode::OK,
557 "Failed to get timeline (3)"
558 );
559 let timeline_body_3: Value = timeline_res_3.json().await.expect("Timeline (3) not JSON");
560 let feed_3 = timeline_body_3["feed"].as_array().unwrap();
561 assert_eq!(feed_3.len(), 1, "Timeline should have 1 post after delete");
562 assert_eq!(
563 feed_3[0]["post"]["uri"], post2_uri,
564 "Only post 2 should remain"
565 );
566}