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