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!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
27 .bearer_auth(&jwt)
28 .json(&payload)
29 .send()
30 .await
31 .expect("Failed to create record");
32
33 assert_eq!(res.status(), StatusCode::OK);
34 let body: Value = res.json().await.unwrap();
35
36 assert!(body["uri"].is_string(), "response must have uri");
37 assert!(body["cid"].is_string(), "response must have cid");
38 assert!(body["cid"].as_str().unwrap().starts_with("bafy"), "cid must be valid");
39
40 assert!(body["commit"].is_object(), "response must have commit object");
41 let commit = &body["commit"];
42 assert!(commit["cid"].is_string(), "commit must have cid");
43 assert!(commit["cid"].as_str().unwrap().starts_with("bafy"), "commit.cid must be valid");
44 assert!(commit["rev"].is_string(), "commit must have rev");
45
46 assert!(body["validationStatus"].is_string(), "response must have validationStatus when validate defaults to true");
47 assert_eq!(body["validationStatus"], "valid", "validationStatus should be 'valid'");
48}
49
50#[tokio::test]
51async fn test_create_record_no_validation_status_when_validate_false() {
52 let client = client();
53 let (did, jwt) = setup_new_user("conform-create-noval").await;
54 let now = Utc::now().to_rfc3339();
55
56 let payload = json!({
57 "repo": did,
58 "collection": "app.bsky.feed.post",
59 "validate": false,
60 "record": {
61 "$type": "app.bsky.feed.post",
62 "text": "Testing without validation",
63 "createdAt": now
64 }
65 });
66
67 let res = client
68 .post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
69 .bearer_auth(&jwt)
70 .json(&payload)
71 .send()
72 .await
73 .expect("Failed to create record");
74
75 assert_eq!(res.status(), StatusCode::OK);
76 let body: Value = res.json().await.unwrap();
77
78 assert!(body["uri"].is_string());
79 assert!(body["commit"].is_object());
80 assert!(body["validationStatus"].is_null(), "validationStatus should be omitted when validate=false");
81}
82
83#[tokio::test]
84async fn test_put_record_response_schema() {
85 let client = client();
86 let (did, jwt) = setup_new_user("conform-put").await;
87 let now = Utc::now().to_rfc3339();
88
89 let payload = json!({
90 "repo": did,
91 "collection": "app.bsky.feed.post",
92 "rkey": "conformance-put",
93 "record": {
94 "$type": "app.bsky.feed.post",
95 "text": "Testing putRecord conformance",
96 "createdAt": now
97 }
98 });
99
100 let res = client
101 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
102 .bearer_auth(&jwt)
103 .json(&payload)
104 .send()
105 .await
106 .expect("Failed to put record");
107
108 assert_eq!(res.status(), StatusCode::OK);
109 let body: Value = res.json().await.unwrap();
110
111 assert!(body["uri"].is_string(), "response must have uri");
112 assert!(body["cid"].is_string(), "response must have cid");
113
114 assert!(body["commit"].is_object(), "response must have commit object");
115 let commit = &body["commit"];
116 assert!(commit["cid"].is_string(), "commit must have cid");
117 assert!(commit["rev"].is_string(), "commit must have rev");
118
119 assert_eq!(body["validationStatus"], "valid", "validationStatus should be 'valid'");
120}
121
122#[tokio::test]
123async fn test_delete_record_response_schema() {
124 let client = client();
125 let (did, jwt) = setup_new_user("conform-delete").await;
126 let now = Utc::now().to_rfc3339();
127
128 let create_payload = json!({
129 "repo": did,
130 "collection": "app.bsky.feed.post",
131 "rkey": "to-delete",
132 "record": {
133 "$type": "app.bsky.feed.post",
134 "text": "This will be deleted",
135 "createdAt": now
136 }
137 });
138 let create_res = client
139 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
140 .bearer_auth(&jwt)
141 .json(&create_payload)
142 .send()
143 .await
144 .expect("Failed to create record");
145 assert_eq!(create_res.status(), StatusCode::OK);
146
147 let delete_payload = json!({
148 "repo": did,
149 "collection": "app.bsky.feed.post",
150 "rkey": "to-delete"
151 });
152 let delete_res = client
153 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
154 .bearer_auth(&jwt)
155 .json(&delete_payload)
156 .send()
157 .await
158 .expect("Failed to delete record");
159
160 assert_eq!(delete_res.status(), StatusCode::OK);
161 let body: Value = delete_res.json().await.unwrap();
162
163 assert!(body["commit"].is_object(), "response must have commit object when record was deleted");
164 let commit = &body["commit"];
165 assert!(commit["cid"].is_string(), "commit must have cid");
166 assert!(commit["rev"].is_string(), "commit must have rev");
167}
168
169#[tokio::test]
170async fn test_delete_record_noop_response() {
171 let client = client();
172 let (did, jwt) = setup_new_user("conform-delete-noop").await;
173
174 let delete_payload = json!({
175 "repo": did,
176 "collection": "app.bsky.feed.post",
177 "rkey": "nonexistent-record"
178 });
179 let delete_res = client
180 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
181 .bearer_auth(&jwt)
182 .json(&delete_payload)
183 .send()
184 .await
185 .expect("Failed to delete record");
186
187 assert_eq!(delete_res.status(), StatusCode::OK);
188 let body: Value = delete_res.json().await.unwrap();
189
190 assert!(body["commit"].is_null(), "commit should be omitted on no-op delete");
191}
192
193#[tokio::test]
194async fn test_apply_writes_response_schema() {
195 let client = client();
196 let (did, jwt) = setup_new_user("conform-apply").await;
197 let now = Utc::now().to_rfc3339();
198
199 let payload = json!({
200 "repo": did,
201 "writes": [
202 {
203 "$type": "com.atproto.repo.applyWrites#create",
204 "collection": "app.bsky.feed.post",
205 "rkey": "apply-test-1",
206 "value": {
207 "$type": "app.bsky.feed.post",
208 "text": "First post",
209 "createdAt": now
210 }
211 },
212 {
213 "$type": "com.atproto.repo.applyWrites#create",
214 "collection": "app.bsky.feed.post",
215 "rkey": "apply-test-2",
216 "value": {
217 "$type": "app.bsky.feed.post",
218 "text": "Second post",
219 "createdAt": now
220 }
221 }
222 ]
223 });
224
225 let res = client
226 .post(format!("{}/xrpc/com.atproto.repo.applyWrites", base_url().await))
227 .bearer_auth(&jwt)
228 .json(&payload)
229 .send()
230 .await
231 .expect("Failed to apply writes");
232
233 assert_eq!(res.status(), StatusCode::OK);
234 let body: Value = res.json().await.unwrap();
235
236 assert!(body["commit"].is_object(), "response must have commit object");
237 let commit = &body["commit"];
238 assert!(commit["cid"].is_string(), "commit must have cid");
239 assert!(commit["rev"].is_string(), "commit must have rev");
240
241 assert!(body["results"].is_array(), "response must have results array");
242 let results = body["results"].as_array().unwrap();
243 assert_eq!(results.len(), 2, "should have 2 results");
244
245 for result in results {
246 assert!(result["uri"].is_string(), "result must have uri");
247 assert!(result["cid"].is_string(), "result must have cid");
248 assert_eq!(result["validationStatus"], "valid", "result must have validationStatus");
249 assert_eq!(result["$type"], "com.atproto.repo.applyWrites#createResult");
250 }
251}
252
253#[tokio::test]
254async fn test_apply_writes_update_and_delete_results() {
255 let client = client();
256 let (did, jwt) = setup_new_user("conform-apply-upd").await;
257 let now = Utc::now().to_rfc3339();
258
259 let create_payload = json!({
260 "repo": did,
261 "collection": "app.bsky.feed.post",
262 "rkey": "to-update",
263 "record": {
264 "$type": "app.bsky.feed.post",
265 "text": "Original",
266 "createdAt": now
267 }
268 });
269 client
270 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
271 .bearer_auth(&jwt)
272 .json(&create_payload)
273 .send()
274 .await
275 .expect("setup failed");
276
277 let payload = json!({
278 "repo": did,
279 "writes": [
280 {
281 "$type": "com.atproto.repo.applyWrites#update",
282 "collection": "app.bsky.feed.post",
283 "rkey": "to-update",
284 "value": {
285 "$type": "app.bsky.feed.post",
286 "text": "Updated",
287 "createdAt": now
288 }
289 },
290 {
291 "$type": "com.atproto.repo.applyWrites#delete",
292 "collection": "app.bsky.feed.post",
293 "rkey": "to-update"
294 }
295 ]
296 });
297
298 let res = client
299 .post(format!("{}/xrpc/com.atproto.repo.applyWrites", base_url().await))
300 .bearer_auth(&jwt)
301 .json(&payload)
302 .send()
303 .await
304 .expect("Failed to apply writes");
305
306 assert_eq!(res.status(), StatusCode::OK);
307 let body: Value = res.json().await.unwrap();
308
309 let results = body["results"].as_array().unwrap();
310 assert_eq!(results.len(), 2);
311
312 let update_result = &results[0];
313 assert_eq!(update_result["$type"], "com.atproto.repo.applyWrites#updateResult");
314 assert!(update_result["uri"].is_string());
315 assert!(update_result["cid"].is_string());
316 assert_eq!(update_result["validationStatus"], "valid");
317
318 let delete_result = &results[1];
319 assert_eq!(delete_result["$type"], "com.atproto.repo.applyWrites#deleteResult");
320 assert!(delete_result["uri"].is_null(), "delete result should not have uri");
321 assert!(delete_result["cid"].is_null(), "delete result should not have cid");
322 assert!(delete_result["validationStatus"].is_null(), "delete result should not have validationStatus");
323}
324
325#[tokio::test]
326async fn test_get_record_error_code() {
327 let client = client();
328 let (did, _jwt) = setup_new_user("conform-get-err").await;
329
330 let res = client
331 .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
332 .query(&[
333 ("repo", did.as_str()),
334 ("collection", "app.bsky.feed.post"),
335 ("rkey", "nonexistent"),
336 ])
337 .send()
338 .await
339 .expect("Failed to get record");
340
341 assert_eq!(res.status(), StatusCode::NOT_FOUND);
342 let body: Value = res.json().await.unwrap();
343 assert_eq!(body["error"], "RecordNotFound", "error code should be RecordNotFound per atproto spec");
344}
345
346#[tokio::test]
347async fn test_create_record_unknown_lexicon_default_validation() {
348 let client = client();
349 let (did, jwt) = setup_new_user("conform-unknown-lex").await;
350
351 let payload = json!({
352 "repo": did,
353 "collection": "com.example.custom",
354 "record": {
355 "$type": "com.example.custom",
356 "data": "some custom data"
357 }
358 });
359
360 let res = client
361 .post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
362 .bearer_auth(&jwt)
363 .json(&payload)
364 .send()
365 .await
366 .expect("Failed to create record");
367
368 assert_eq!(res.status(), StatusCode::OK, "unknown lexicon should be allowed with default validation");
369 let body: Value = res.json().await.unwrap();
370
371 assert!(body["uri"].is_string());
372 assert!(body["cid"].is_string());
373 assert!(body["commit"].is_object());
374 assert_eq!(body["validationStatus"], "unknown", "validationStatus should be 'unknown' for unknown lexicons");
375}
376
377#[tokio::test]
378async fn test_create_record_unknown_lexicon_strict_validation() {
379 let client = client();
380 let (did, jwt) = setup_new_user("conform-unknown-strict").await;
381
382 let payload = json!({
383 "repo": did,
384 "collection": "com.example.custom",
385 "validate": true,
386 "record": {
387 "$type": "com.example.custom",
388 "data": "some custom data"
389 }
390 });
391
392 let res = client
393 .post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await))
394 .bearer_auth(&jwt)
395 .json(&payload)
396 .send()
397 .await
398 .expect("Failed to send request");
399
400 assert_eq!(res.status(), StatusCode::BAD_REQUEST, "unknown lexicon should fail with validate=true");
401 let body: Value = res.json().await.unwrap();
402 assert_eq!(body["error"], "InvalidRecord");
403 assert!(body["message"].as_str().unwrap().contains("Lexicon not found"), "error should mention lexicon not found");
404}
405
406#[tokio::test]
407async fn test_put_record_noop_same_content() {
408 let client = client();
409 let (did, jwt) = setup_new_user("conform-put-noop").await;
410 let now = Utc::now().to_rfc3339();
411
412 let record = json!({
413 "$type": "app.bsky.feed.post",
414 "text": "This content will not change",
415 "createdAt": now
416 });
417
418 let payload = json!({
419 "repo": did,
420 "collection": "app.bsky.feed.post",
421 "rkey": "noop-test",
422 "record": record.clone()
423 });
424
425 let first_res = client
426 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
427 .bearer_auth(&jwt)
428 .json(&payload)
429 .send()
430 .await
431 .expect("Failed to put record");
432 assert_eq!(first_res.status(), StatusCode::OK);
433 let first_body: Value = first_res.json().await.unwrap();
434 assert!(first_body["commit"].is_object(), "first put should have commit");
435
436 let second_res = client
437 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
438 .bearer_auth(&jwt)
439 .json(&payload)
440 .send()
441 .await
442 .expect("Failed to put record");
443 assert_eq!(second_res.status(), StatusCode::OK);
444 let second_body: Value = second_res.json().await.unwrap();
445
446 assert!(second_body["commit"].is_null(), "second put with same content should have no commit (no-op)");
447 assert_eq!(first_body["cid"], second_body["cid"], "CID should be the same for identical content");
448}
449
450#[tokio::test]
451async fn test_apply_writes_unknown_lexicon() {
452 let client = client();
453 let (did, jwt) = setup_new_user("conform-apply-unknown").await;
454
455 let payload = json!({
456 "repo": did,
457 "writes": [
458 {
459 "$type": "com.atproto.repo.applyWrites#create",
460 "collection": "com.example.custom",
461 "rkey": "custom-1",
462 "value": {
463 "$type": "com.example.custom",
464 "data": "custom data"
465 }
466 }
467 ]
468 });
469
470 let res = client
471 .post(format!("{}/xrpc/com.atproto.repo.applyWrites", base_url().await))
472 .bearer_auth(&jwt)
473 .json(&payload)
474 .send()
475 .await
476 .expect("Failed to apply writes");
477
478 assert_eq!(res.status(), StatusCode::OK);
479 let body: Value = res.json().await.unwrap();
480
481 let results = body["results"].as_array().unwrap();
482 assert_eq!(results.len(), 1);
483 assert_eq!(results[0]["validationStatus"], "unknown", "unknown lexicon should have 'unknown' status");
484}