this repo has no description
1mod common;
2mod helpers;
3use chrono::Utc;
4use common::*;
5use helpers::*;
6use reqwest::StatusCode;
7use serde_json::{Value, json};
8
9#[tokio::test]
10async fn test_create_record_response_schema() {
11 let client = client();
12 let (did, jwt) = setup_new_user("conform-create").await;
13 let now = Utc::now().to_rfc3339();
14
15 let payload = json!({
16 "repo": did,
17 "collection": "app.bsky.feed.post",
18 "record": {
19 "$type": "app.bsky.feed.post",
20 "text": "Testing conformance",
21 "createdAt": now
22 }
23 });
24
25 let res = client
26 .post(format!(
27 "{}/xrpc/com.atproto.repo.createRecord",
28 base_url().await
29 ))
30 .bearer_auth(&jwt)
31 .json(&payload)
32 .send()
33 .await
34 .expect("Failed to create record");
35
36 assert_eq!(res.status(), StatusCode::OK);
37 let body: Value = res.json().await.unwrap();
38
39 assert!(body["uri"].is_string(), "response must have uri");
40 assert!(body["cid"].is_string(), "response must have cid");
41 assert!(
42 body["cid"].as_str().unwrap().starts_with("bafy"),
43 "cid must be valid"
44 );
45
46 assert!(
47 body["commit"].is_object(),
48 "response must have commit object"
49 );
50 let commit = &body["commit"];
51 assert!(commit["cid"].is_string(), "commit must have cid");
52 assert!(
53 commit["cid"].as_str().unwrap().starts_with("bafy"),
54 "commit.cid must be valid"
55 );
56 assert!(commit["rev"].is_string(), "commit must have rev");
57
58 assert!(
59 body["validationStatus"].is_string(),
60 "response must have validationStatus when validate defaults to true"
61 );
62 assert_eq!(
63 body["validationStatus"], "valid",
64 "validationStatus should be 'valid'"
65 );
66}
67
68#[tokio::test]
69async fn test_create_record_no_validation_status_when_validate_false() {
70 let client = client();
71 let (did, jwt) = setup_new_user("conform-create-noval").await;
72 let now = Utc::now().to_rfc3339();
73
74 let payload = json!({
75 "repo": did,
76 "collection": "app.bsky.feed.post",
77 "validate": false,
78 "record": {
79 "$type": "app.bsky.feed.post",
80 "text": "Testing without validation",
81 "createdAt": now
82 }
83 });
84
85 let res = client
86 .post(format!(
87 "{}/xrpc/com.atproto.repo.createRecord",
88 base_url().await
89 ))
90 .bearer_auth(&jwt)
91 .json(&payload)
92 .send()
93 .await
94 .expect("Failed to create record");
95
96 assert_eq!(res.status(), StatusCode::OK);
97 let body: Value = res.json().await.unwrap();
98
99 assert!(body["uri"].is_string());
100 assert!(body["commit"].is_object());
101 assert!(
102 body["validationStatus"].is_null(),
103 "validationStatus should be omitted when validate=false"
104 );
105}
106
107#[tokio::test]
108async fn test_put_record_response_schema() {
109 let client = client();
110 let (did, jwt) = setup_new_user("conform-put").await;
111 let now = Utc::now().to_rfc3339();
112
113 let payload = json!({
114 "repo": did,
115 "collection": "app.bsky.feed.post",
116 "rkey": "conformance-put",
117 "record": {
118 "$type": "app.bsky.feed.post",
119 "text": "Testing putRecord conformance",
120 "createdAt": now
121 }
122 });
123
124 let res = client
125 .post(format!(
126 "{}/xrpc/com.atproto.repo.putRecord",
127 base_url().await
128 ))
129 .bearer_auth(&jwt)
130 .json(&payload)
131 .send()
132 .await
133 .expect("Failed to put record");
134
135 assert_eq!(res.status(), StatusCode::OK);
136 let body: Value = res.json().await.unwrap();
137
138 assert!(body["uri"].is_string(), "response must have uri");
139 assert!(body["cid"].is_string(), "response must have cid");
140
141 assert!(
142 body["commit"].is_object(),
143 "response must have commit object"
144 );
145 let commit = &body["commit"];
146 assert!(commit["cid"].is_string(), "commit must have cid");
147 assert!(commit["rev"].is_string(), "commit must have rev");
148
149 assert_eq!(
150 body["validationStatus"], "valid",
151 "validationStatus should be 'valid'"
152 );
153}
154
155#[tokio::test]
156async fn test_delete_record_response_schema() {
157 let client = client();
158 let (did, jwt) = setup_new_user("conform-delete").await;
159 let now = Utc::now().to_rfc3339();
160
161 let create_payload = json!({
162 "repo": did,
163 "collection": "app.bsky.feed.post",
164 "rkey": "to-delete",
165 "record": {
166 "$type": "app.bsky.feed.post",
167 "text": "This will be deleted",
168 "createdAt": now
169 }
170 });
171 let create_res = client
172 .post(format!(
173 "{}/xrpc/com.atproto.repo.putRecord",
174 base_url().await
175 ))
176 .bearer_auth(&jwt)
177 .json(&create_payload)
178 .send()
179 .await
180 .expect("Failed to create record");
181 assert_eq!(create_res.status(), StatusCode::OK);
182
183 let delete_payload = json!({
184 "repo": did,
185 "collection": "app.bsky.feed.post",
186 "rkey": "to-delete"
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 delete record");
198
199 assert_eq!(delete_res.status(), StatusCode::OK);
200 let body: Value = delete_res.json().await.unwrap();
201
202 assert!(
203 body["commit"].is_object(),
204 "response must have commit object when record was deleted"
205 );
206 let commit = &body["commit"];
207 assert!(commit["cid"].is_string(), "commit must have cid");
208 assert!(commit["rev"].is_string(), "commit must have rev");
209}
210
211#[tokio::test]
212async fn test_delete_record_noop_response() {
213 let client = client();
214 let (did, jwt) = setup_new_user("conform-delete-noop").await;
215
216 let delete_payload = json!({
217 "repo": did,
218 "collection": "app.bsky.feed.post",
219 "rkey": "nonexistent-record"
220 });
221 let delete_res = client
222 .post(format!(
223 "{}/xrpc/com.atproto.repo.deleteRecord",
224 base_url().await
225 ))
226 .bearer_auth(&jwt)
227 .json(&delete_payload)
228 .send()
229 .await
230 .expect("Failed to delete record");
231
232 assert_eq!(delete_res.status(), StatusCode::OK);
233 let body: Value = delete_res.json().await.unwrap();
234
235 assert!(
236 body["commit"].is_null(),
237 "commit should be omitted on no-op delete"
238 );
239}
240
241#[tokio::test]
242async fn test_apply_writes_response_schema() {
243 let client = client();
244 let (did, jwt) = setup_new_user("conform-apply").await;
245 let now = Utc::now().to_rfc3339();
246
247 let payload = json!({
248 "repo": did,
249 "writes": [
250 {
251 "$type": "com.atproto.repo.applyWrites#create",
252 "collection": "app.bsky.feed.post",
253 "rkey": "apply-test-1",
254 "value": {
255 "$type": "app.bsky.feed.post",
256 "text": "First post",
257 "createdAt": now
258 }
259 },
260 {
261 "$type": "com.atproto.repo.applyWrites#create",
262 "collection": "app.bsky.feed.post",
263 "rkey": "apply-test-2",
264 "value": {
265 "$type": "app.bsky.feed.post",
266 "text": "Second post",
267 "createdAt": now
268 }
269 }
270 ]
271 });
272
273 let res = client
274 .post(format!(
275 "{}/xrpc/com.atproto.repo.applyWrites",
276 base_url().await
277 ))
278 .bearer_auth(&jwt)
279 .json(&payload)
280 .send()
281 .await
282 .expect("Failed to apply writes");
283
284 assert_eq!(res.status(), StatusCode::OK);
285 let body: Value = res.json().await.unwrap();
286
287 assert!(
288 body["commit"].is_object(),
289 "response must have commit object"
290 );
291 let commit = &body["commit"];
292 assert!(commit["cid"].is_string(), "commit must have cid");
293 assert!(commit["rev"].is_string(), "commit must have rev");
294
295 assert!(
296 body["results"].is_array(),
297 "response must have results array"
298 );
299 let results = body["results"].as_array().unwrap();
300 assert_eq!(results.len(), 2, "should have 2 results");
301
302 for result in results {
303 assert!(result["uri"].is_string(), "result must have uri");
304 assert!(result["cid"].is_string(), "result must have cid");
305 assert_eq!(
306 result["validationStatus"], "valid",
307 "result must have validationStatus"
308 );
309 assert_eq!(result["$type"], "com.atproto.repo.applyWrites#createResult");
310 }
311}
312
313#[tokio::test]
314async fn test_apply_writes_update_and_delete_results() {
315 let client = client();
316 let (did, jwt) = setup_new_user("conform-apply-upd").await;
317 let now = Utc::now().to_rfc3339();
318
319 let create_payload = json!({
320 "repo": did,
321 "collection": "app.bsky.feed.post",
322 "rkey": "to-update",
323 "record": {
324 "$type": "app.bsky.feed.post",
325 "text": "Original",
326 "createdAt": now
327 }
328 });
329 client
330 .post(format!(
331 "{}/xrpc/com.atproto.repo.putRecord",
332 base_url().await
333 ))
334 .bearer_auth(&jwt)
335 .json(&create_payload)
336 .send()
337 .await
338 .expect("setup failed");
339
340 let payload = json!({
341 "repo": did,
342 "writes": [
343 {
344 "$type": "com.atproto.repo.applyWrites#update",
345 "collection": "app.bsky.feed.post",
346 "rkey": "to-update",
347 "value": {
348 "$type": "app.bsky.feed.post",
349 "text": "Updated",
350 "createdAt": now
351 }
352 },
353 {
354 "$type": "com.atproto.repo.applyWrites#delete",
355 "collection": "app.bsky.feed.post",
356 "rkey": "to-update"
357 }
358 ]
359 });
360
361 let res = client
362 .post(format!(
363 "{}/xrpc/com.atproto.repo.applyWrites",
364 base_url().await
365 ))
366 .bearer_auth(&jwt)
367 .json(&payload)
368 .send()
369 .await
370 .expect("Failed to apply writes");
371
372 assert_eq!(res.status(), StatusCode::OK);
373 let body: Value = res.json().await.unwrap();
374
375 let results = body["results"].as_array().unwrap();
376 assert_eq!(results.len(), 2);
377
378 let update_result = &results[0];
379 assert_eq!(
380 update_result["$type"],
381 "com.atproto.repo.applyWrites#updateResult"
382 );
383 assert!(update_result["uri"].is_string());
384 assert!(update_result["cid"].is_string());
385 assert_eq!(update_result["validationStatus"], "valid");
386
387 let delete_result = &results[1];
388 assert_eq!(
389 delete_result["$type"],
390 "com.atproto.repo.applyWrites#deleteResult"
391 );
392 assert!(
393 delete_result["uri"].is_null(),
394 "delete result should not have uri"
395 );
396 assert!(
397 delete_result["cid"].is_null(),
398 "delete result should not have cid"
399 );
400 assert!(
401 delete_result["validationStatus"].is_null(),
402 "delete result should not have validationStatus"
403 );
404}
405
406#[tokio::test]
407async fn test_get_record_error_code() {
408 let client = client();
409 let (did, _jwt) = setup_new_user("conform-get-err").await;
410
411 let res = client
412 .get(format!(
413 "{}/xrpc/com.atproto.repo.getRecord",
414 base_url().await
415 ))
416 .query(&[
417 ("repo", did.as_str()),
418 ("collection", "app.bsky.feed.post"),
419 ("rkey", "nonexistent"),
420 ])
421 .send()
422 .await
423 .expect("Failed to get record");
424
425 assert_eq!(res.status(), StatusCode::NOT_FOUND);
426 let body: Value = res.json().await.unwrap();
427 assert_eq!(
428 body["error"], "RecordNotFound",
429 "error code should be RecordNotFound per atproto spec"
430 );
431}
432
433#[tokio::test]
434async fn test_create_record_unknown_lexicon_default_validation() {
435 let client = client();
436 let (did, jwt) = setup_new_user("conform-unknown-lex").await;
437
438 let payload = json!({
439 "repo": did,
440 "collection": "com.example.custom",
441 "record": {
442 "$type": "com.example.custom",
443 "data": "some custom data"
444 }
445 });
446
447 let res = client
448 .post(format!(
449 "{}/xrpc/com.atproto.repo.createRecord",
450 base_url().await
451 ))
452 .bearer_auth(&jwt)
453 .json(&payload)
454 .send()
455 .await
456 .expect("Failed to create record");
457
458 assert_eq!(
459 res.status(),
460 StatusCode::OK,
461 "unknown lexicon should be allowed with default validation"
462 );
463 let body: Value = res.json().await.unwrap();
464
465 assert!(body["uri"].is_string());
466 assert!(body["cid"].is_string());
467 assert!(body["commit"].is_object());
468 assert_eq!(
469 body["validationStatus"], "unknown",
470 "validationStatus should be 'unknown' for unknown lexicons"
471 );
472}
473
474#[tokio::test]
475async fn test_create_record_unknown_lexicon_strict_validation() {
476 let client = client();
477 let (did, jwt) = setup_new_user("conform-unknown-strict").await;
478
479 let payload = json!({
480 "repo": did,
481 "collection": "com.example.custom",
482 "validate": true,
483 "record": {
484 "$type": "com.example.custom",
485 "data": "some custom data"
486 }
487 });
488
489 let res = client
490 .post(format!(
491 "{}/xrpc/com.atproto.repo.createRecord",
492 base_url().await
493 ))
494 .bearer_auth(&jwt)
495 .json(&payload)
496 .send()
497 .await
498 .expect("Failed to send request");
499
500 assert_eq!(
501 res.status(),
502 StatusCode::BAD_REQUEST,
503 "unknown lexicon should fail with validate=true"
504 );
505 let body: Value = res.json().await.unwrap();
506 assert_eq!(body["error"], "InvalidRecord");
507 assert!(
508 body["message"]
509 .as_str()
510 .unwrap()
511 .contains("Lexicon not found"),
512 "error should mention lexicon not found"
513 );
514}
515
516#[tokio::test]
517async fn test_put_record_noop_same_content() {
518 let client = client();
519 let (did, jwt) = setup_new_user("conform-put-noop").await;
520 let now = Utc::now().to_rfc3339();
521
522 let record = json!({
523 "$type": "app.bsky.feed.post",
524 "text": "This content will not change",
525 "createdAt": now
526 });
527
528 let payload = json!({
529 "repo": did,
530 "collection": "app.bsky.feed.post",
531 "rkey": "noop-test",
532 "record": record.clone()
533 });
534
535 let first_res = client
536 .post(format!(
537 "{}/xrpc/com.atproto.repo.putRecord",
538 base_url().await
539 ))
540 .bearer_auth(&jwt)
541 .json(&payload)
542 .send()
543 .await
544 .expect("Failed to put record");
545 assert_eq!(first_res.status(), StatusCode::OK);
546 let first_body: Value = first_res.json().await.unwrap();
547 assert!(
548 first_body["commit"].is_object(),
549 "first put should have commit"
550 );
551
552 let second_res = client
553 .post(format!(
554 "{}/xrpc/com.atproto.repo.putRecord",
555 base_url().await
556 ))
557 .bearer_auth(&jwt)
558 .json(&payload)
559 .send()
560 .await
561 .expect("Failed to put record");
562 assert_eq!(second_res.status(), StatusCode::OK);
563 let second_body: Value = second_res.json().await.unwrap();
564
565 assert!(
566 second_body["commit"].is_null(),
567 "second put with same content should have no commit (no-op)"
568 );
569 assert_eq!(
570 first_body["cid"], second_body["cid"],
571 "CID should be the same for identical content"
572 );
573}
574
575#[tokio::test]
576async fn test_apply_writes_unknown_lexicon() {
577 let client = client();
578 let (did, jwt) = setup_new_user("conform-apply-unknown").await;
579
580 let payload = json!({
581 "repo": did,
582 "writes": [
583 {
584 "$type": "com.atproto.repo.applyWrites#create",
585 "collection": "com.example.custom",
586 "rkey": "custom-1",
587 "value": {
588 "$type": "com.example.custom",
589 "data": "custom data"
590 }
591 }
592 ]
593 });
594
595 let res = client
596 .post(format!(
597 "{}/xrpc/com.atproto.repo.applyWrites",
598 base_url().await
599 ))
600 .bearer_auth(&jwt)
601 .json(&payload)
602 .send()
603 .await
604 .expect("Failed to apply writes");
605
606 assert_eq!(res.status(), StatusCode::OK);
607 let body: Value = res.json().await.unwrap();
608
609 let results = body["results"].as_array().unwrap();
610 assert_eq!(results.len(), 1);
611 assert_eq!(
612 results[0]["validationStatus"], "unknown",
613 "unknown lexicon should have 'unknown' status"
614 );
615}