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