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