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 assert_eq!(import_res.status(), StatusCode::OK);
160}
161
162#[tokio::test]
163async fn test_import_repo_size_limit() {
164 let client = client();
165 let (token, _did) = create_account_and_login(&client).await;
166 let oversized_body = vec![0u8; 110 * 1024 * 1024];
167 let res = client
168 .post(format!(
169 "{}/xrpc/com.atproto.repo.importRepo",
170 base_url().await
171 ))
172 .bearer_auth(&token)
173 .header("Content-Type", "application/vnd.ipld.car")
174 .body(oversized_body)
175 .send()
176 .await;
177 match res {
178 Ok(response) => {
179 assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
180 }
181 Err(e) => {
182 let error_str = e.to_string().to_lowercase();
183 assert!(
184 error_str.contains("broken pipe")
185 || error_str.contains("connection")
186 || error_str.contains("reset")
187 || error_str.contains("request")
188 || error_str.contains("body"),
189 "Expected connection error or PAYLOAD_TOO_LARGE, got: {}",
190 e
191 );
192 }
193 }
194}
195
196#[tokio::test]
197async fn test_import_deactivated_account_allowed_for_migration() {
198 let client = client();
199 let (token, did) = create_account_and_login(&client).await;
200 let export_res = client
201 .get(format!(
202 "{}/xrpc/com.atproto.sync.getRepo?did={}",
203 base_url().await,
204 did
205 ))
206 .send()
207 .await
208 .expect("Export failed");
209 assert_eq!(export_res.status(), StatusCode::OK);
210 let car_bytes = export_res.bytes().await.unwrap();
211 let deactivate_res = client
212 .post(format!(
213 "{}/xrpc/com.atproto.server.deactivateAccount",
214 base_url().await
215 ))
216 .bearer_auth(&token)
217 .json(&json!({}))
218 .send()
219 .await
220 .expect("Deactivate failed");
221 assert!(deactivate_res.status().is_success());
222 let import_res = client
223 .post(format!(
224 "{}/xrpc/com.atproto.repo.importRepo",
225 base_url().await
226 ))
227 .bearer_auth(&token)
228 .header("Content-Type", "application/vnd.ipld.car")
229 .body(car_bytes.to_vec())
230 .send()
231 .await
232 .expect("Import failed");
233 assert!(
234 import_res.status().is_success(),
235 "Deactivated accounts should allow import for migration, got {}",
236 import_res.status()
237 );
238}
239
240#[tokio::test]
241async fn test_import_invalid_car_structure() {
242 let client = client();
243 let (token, _did) = create_account_and_login(&client).await;
244 let invalid_car = vec![0x0a, 0xa1, 0x65, 0x72, 0x6f, 0x6f, 0x74, 0x73, 0x80];
245 let res = client
246 .post(format!(
247 "{}/xrpc/com.atproto.repo.importRepo",
248 base_url().await
249 ))
250 .bearer_auth(&token)
251 .header("Content-Type", "application/vnd.ipld.car")
252 .body(invalid_car)
253 .send()
254 .await
255 .expect("Request failed");
256 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
257}
258
259#[tokio::test]
260async fn test_import_car_with_no_roots() {
261 let client = client();
262 let (token, _did) = create_account_and_login(&client).await;
263 let header = CarHeader::new_v1(vec![]);
264 let header_cbor = header.encode().unwrap_or_default();
265 let mut car = Vec::new();
266 write_varint(&mut car, header_cbor.len() as u64);
267 car.extend_from_slice(&header_cbor);
268 let res = client
269 .post(format!(
270 "{}/xrpc/com.atproto.repo.importRepo",
271 base_url().await
272 ))
273 .bearer_auth(&token)
274 .header("Content-Type", "application/vnd.ipld.car")
275 .body(car)
276 .send()
277 .await
278 .expect("Request failed");
279 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
280 let body: serde_json::Value = res.json().await.unwrap();
281 assert_eq!(body["error"], "InvalidRequest");
282}
283
284#[tokio::test]
285async fn test_import_preserves_records_after_reimport() {
286 let client = client();
287 let (token, did) = create_account_and_login(&client).await;
288 let mut rkeys = Vec::new();
289 for i in 0..3 {
290 let post_payload = json!({
291 "repo": did,
292 "collection": "app.bsky.feed.post",
293 "record": {
294 "$type": "app.bsky.feed.post",
295 "text": format!("Test post {}", i),
296 "createdAt": chrono::Utc::now().to_rfc3339(),
297 }
298 });
299 let res = client
300 .post(format!(
301 "{}/xrpc/com.atproto.repo.createRecord",
302 base_url().await
303 ))
304 .bearer_auth(&token)
305 .json(&post_payload)
306 .send()
307 .await
308 .expect("Failed to create post");
309 assert_eq!(res.status(), StatusCode::OK);
310 let body: serde_json::Value = res.json().await.unwrap();
311 let uri = body["uri"].as_str().unwrap();
312 let rkey = uri.split('/').next_back().unwrap().to_string();
313 rkeys.push(rkey);
314 }
315 for rkey in &rkeys {
316 let get_res = client
317 .get(format!(
318 "{}/xrpc/com.atproto.repo.getRecord?repo={}&collection=app.bsky.feed.post&rkey={}",
319 base_url().await,
320 did,
321 rkey
322 ))
323 .send()
324 .await
325 .expect("Failed to get record before export");
326 assert_eq!(
327 get_res.status(),
328 StatusCode::OK,
329 "Record {} not found before export",
330 rkey
331 );
332 }
333 let export_res = client
334 .get(format!(
335 "{}/xrpc/com.atproto.sync.getRepo?did={}",
336 base_url().await,
337 did
338 ))
339 .send()
340 .await
341 .expect("Failed to export repo");
342 assert_eq!(export_res.status(), StatusCode::OK);
343 let car_bytes = export_res.bytes().await.unwrap();
344 let import_res = client
345 .post(format!(
346 "{}/xrpc/com.atproto.repo.importRepo",
347 base_url().await
348 ))
349 .bearer_auth(&token)
350 .header("Content-Type", "application/vnd.ipld.car")
351 .body(car_bytes.to_vec())
352 .send()
353 .await
354 .expect("Failed to import repo");
355 assert_eq!(import_res.status(), StatusCode::OK);
356 let list_res = client
357 .get(format!(
358 "{}/xrpc/com.atproto.repo.listRecords?repo={}&collection=app.bsky.feed.post",
359 base_url().await,
360 did
361 ))
362 .send()
363 .await
364 .expect("Failed to list records after import");
365 assert_eq!(list_res.status(), StatusCode::OK);
366 let list_body: serde_json::Value = list_res.json().await.unwrap();
367 let records_after = list_body["records"]
368 .as_array()
369 .map(|a| a.len())
370 .unwrap_or(0);
371 assert!(
372 records_after >= 1,
373 "Expected at least 1 record after import, found {}. Note: MST walk may have timing issues.",
374 records_after
375 );
376}