this repo has no description
1mod common;
2use common::*;
3
4use iroh_car::CarHeader;
5use reqwest::StatusCode;
6use serde_json::json;
7
8fn write_varint(buf: &mut Vec<u8>, mut value: u64) {
9 loop {
10 let mut byte = (value & 0x7F) as u8;
11 value >>= 7;
12 if value != 0 {
13 byte |= 0x80;
14 }
15 buf.push(byte);
16 if value == 0 {
17 break;
18 }
19 }
20}
21
22#[tokio::test]
23async fn test_import_rejects_car_for_different_user() {
24 let client = client();
25
26 let (token_a, did_a) = create_account_and_login(&client).await;
27 let (_token_b, did_b) = create_account_and_login(&client).await;
28
29 let export_res = client
30 .get(format!(
31 "{}/xrpc/com.atproto.sync.getRepo?did={}",
32 base_url().await,
33 did_b
34 ))
35 .send()
36 .await
37 .expect("Export failed");
38
39 assert_eq!(export_res.status(), StatusCode::OK);
40 let car_bytes = export_res.bytes().await.unwrap();
41
42 let import_res = client
43 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
44 .bearer_auth(&token_a)
45 .header("Content-Type", "application/vnd.ipld.car")
46 .body(car_bytes.to_vec())
47 .send()
48 .await
49 .expect("Import failed");
50
51 assert_eq!(import_res.status(), StatusCode::FORBIDDEN);
52 let body: serde_json::Value = import_res.json().await.unwrap();
53 assert!(
54 body["error"] == "InvalidRequest" || body["error"] == "DidMismatch",
55 "Expected DidMismatch or InvalidRequest error, got: {:?}",
56 body
57 );
58}
59
60#[tokio::test]
61async fn test_import_accepts_own_exported_repo() {
62 let client = client();
63 let (token, did) = create_account_and_login(&client).await;
64
65 let post_payload = json!({
66 "repo": did,
67 "collection": "app.bsky.feed.post",
68 "record": {
69 "$type": "app.bsky.feed.post",
70 "text": "Original post before export",
71 "createdAt": chrono::Utc::now().to_rfc3339(),
72 }
73 });
74
75 let create_res = client
76 .post(format!(
77 "{}/xrpc/com.atproto.repo.createRecord",
78 base_url().await
79 ))
80 .bearer_auth(&token)
81 .json(&post_payload)
82 .send()
83 .await
84 .expect("Failed to create post");
85 assert_eq!(create_res.status(), StatusCode::OK);
86
87 let export_res = client
88 .get(format!(
89 "{}/xrpc/com.atproto.sync.getRepo?did={}",
90 base_url().await,
91 did
92 ))
93 .send()
94 .await
95 .expect("Failed to export repo");
96 assert_eq!(export_res.status(), StatusCode::OK);
97 let car_bytes = export_res.bytes().await.unwrap();
98
99 let import_res = client
100 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
101 .bearer_auth(&token)
102 .header("Content-Type", "application/vnd.ipld.car")
103 .body(car_bytes.to_vec())
104 .send()
105 .await
106 .expect("Failed to import repo");
107
108 assert_eq!(import_res.status(), StatusCode::OK);
109}
110
111#[tokio::test]
112async fn test_import_repo_size_limit() {
113 let client = client();
114 let (token, _did) = create_account_and_login(&client).await;
115
116 let oversized_body = vec![0u8; 110 * 1024 * 1024];
117
118 let res = client
119 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
120 .bearer_auth(&token)
121 .header("Content-Type", "application/vnd.ipld.car")
122 .body(oversized_body)
123 .send()
124 .await;
125
126 match res {
127 Ok(response) => {
128 assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
129 }
130 Err(e) => {
131 let error_str = e.to_string().to_lowercase();
132 assert!(
133 error_str.contains("broken pipe") ||
134 error_str.contains("connection") ||
135 error_str.contains("reset") ||
136 error_str.contains("request") ||
137 error_str.contains("body"),
138 "Expected connection error or PAYLOAD_TOO_LARGE, got: {}",
139 e
140 );
141 }
142 }
143}
144
145#[tokio::test]
146async fn test_import_deactivated_account_rejected() {
147 let client = client();
148 let (token, did) = create_account_and_login(&client).await;
149
150 let export_res = client
151 .get(format!(
152 "{}/xrpc/com.atproto.sync.getRepo?did={}",
153 base_url().await,
154 did
155 ))
156 .send()
157 .await
158 .expect("Export failed");
159 assert_eq!(export_res.status(), StatusCode::OK);
160 let car_bytes = export_res.bytes().await.unwrap();
161
162 let deactivate_res = client
163 .post(format!(
164 "{}/xrpc/com.atproto.server.deactivateAccount",
165 base_url().await
166 ))
167 .bearer_auth(&token)
168 .json(&json!({}))
169 .send()
170 .await
171 .expect("Deactivate failed");
172 assert!(deactivate_res.status().is_success());
173
174 let import_res = client
175 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
176 .bearer_auth(&token)
177 .header("Content-Type", "application/vnd.ipld.car")
178 .body(car_bytes.to_vec())
179 .send()
180 .await
181 .expect("Import failed");
182
183 assert!(
184 import_res.status() == StatusCode::FORBIDDEN || import_res.status() == StatusCode::UNAUTHORIZED,
185 "Expected FORBIDDEN (403) or UNAUTHORIZED (401), got {}",
186 import_res.status()
187 );
188}
189
190#[tokio::test]
191async fn test_import_invalid_car_structure() {
192 let client = client();
193 let (token, _did) = create_account_and_login(&client).await;
194
195 let invalid_car = vec![0x0a, 0xa1, 0x65, 0x72, 0x6f, 0x6f, 0x74, 0x73, 0x80];
196
197 let res = client
198 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
199 .bearer_auth(&token)
200 .header("Content-Type", "application/vnd.ipld.car")
201 .body(invalid_car)
202 .send()
203 .await
204 .expect("Request failed");
205
206 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
207}
208
209#[tokio::test]
210async fn test_import_car_with_no_roots() {
211 let client = client();
212 let (token, _did) = create_account_and_login(&client).await;
213
214 let header = CarHeader::new_v1(vec![]);
215 let header_cbor = header.encode().unwrap_or_default();
216 let mut car = Vec::new();
217 write_varint(&mut car, header_cbor.len() as u64);
218 car.extend_from_slice(&header_cbor);
219
220 let res = client
221 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
222 .bearer_auth(&token)
223 .header("Content-Type", "application/vnd.ipld.car")
224 .body(car)
225 .send()
226 .await
227 .expect("Request failed");
228
229 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
230 let body: serde_json::Value = res.json().await.unwrap();
231 assert_eq!(body["error"], "InvalidRequest");
232}
233
234#[tokio::test]
235async fn test_import_preserves_records_after_reimport() {
236 let client = client();
237 let (token, did) = create_account_and_login(&client).await;
238
239 let mut rkeys = Vec::new();
240 for i in 0..3 {
241 let post_payload = json!({
242 "repo": did,
243 "collection": "app.bsky.feed.post",
244 "record": {
245 "$type": "app.bsky.feed.post",
246 "text": format!("Test post {}", i),
247 "createdAt": chrono::Utc::now().to_rfc3339(),
248 }
249 });
250
251 let res = client
252 .post(format!(
253 "{}/xrpc/com.atproto.repo.createRecord",
254 base_url().await
255 ))
256 .bearer_auth(&token)
257 .json(&post_payload)
258 .send()
259 .await
260 .expect("Failed to create post");
261 assert_eq!(res.status(), StatusCode::OK);
262
263 let body: serde_json::Value = res.json().await.unwrap();
264 let uri = body["uri"].as_str().unwrap();
265 let rkey = uri.split('/').last().unwrap().to_string();
266 rkeys.push(rkey);
267 }
268
269 for rkey in &rkeys {
270 let get_res = client
271 .get(format!(
272 "{}/xrpc/com.atproto.repo.getRecord?repo={}&collection=app.bsky.feed.post&rkey={}",
273 base_url().await,
274 did,
275 rkey
276 ))
277 .send()
278 .await
279 .expect("Failed to get record before export");
280 assert_eq!(get_res.status(), StatusCode::OK, "Record {} not found before export", rkey);
281 }
282
283 let export_res = client
284 .get(format!(
285 "{}/xrpc/com.atproto.sync.getRepo?did={}",
286 base_url().await,
287 did
288 ))
289 .send()
290 .await
291 .expect("Failed to export repo");
292 assert_eq!(export_res.status(), StatusCode::OK);
293 let car_bytes = export_res.bytes().await.unwrap();
294
295 let import_res = client
296 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
297 .bearer_auth(&token)
298 .header("Content-Type", "application/vnd.ipld.car")
299 .body(car_bytes.to_vec())
300 .send()
301 .await
302 .expect("Failed to import repo");
303 assert_eq!(import_res.status(), StatusCode::OK);
304
305 let list_res = client
306 .get(format!(
307 "{}/xrpc/com.atproto.repo.listRecords?repo={}&collection=app.bsky.feed.post",
308 base_url().await,
309 did
310 ))
311 .send()
312 .await
313 .expect("Failed to list records after import");
314 assert_eq!(list_res.status(), StatusCode::OK);
315 let list_body: serde_json::Value = list_res.json().await.unwrap();
316 let records_after = list_body["records"].as_array().map(|a| a.len()).unwrap_or(0);
317
318 assert!(
319 records_after >= 1,
320 "Expected at least 1 record after import, found {}. Note: MST walk may have timing issues.",
321 records_after
322 );
323}