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"] == "InvalidRequest" || body["error"] == "DidMismatch",
106 "Expected 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_rejected() {
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() == StatusCode::FORBIDDEN
233 || import_res.status() == StatusCode::UNAUTHORIZED,
234 "Expected FORBIDDEN (403) or UNAUTHORIZED (401), got {}",
235 import_res.status()
236 );
237}
238
239#[tokio::test]
240async fn test_import_invalid_car_structure() {
241 let client = client();
242 let (token, _did) = create_account_and_login(&client).await;
243 let invalid_car = vec![0x0a, 0xa1, 0x65, 0x72, 0x6f, 0x6f, 0x74, 0x73, 0x80];
244 let res = client
245 .post(format!(
246 "{}/xrpc/com.atproto.repo.importRepo",
247 base_url().await
248 ))
249 .bearer_auth(&token)
250 .header("Content-Type", "application/vnd.ipld.car")
251 .body(invalid_car)
252 .send()
253 .await
254 .expect("Request failed");
255 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
256}
257
258#[tokio::test]
259async fn test_import_car_with_no_roots() {
260 let client = client();
261 let (token, _did) = create_account_and_login(&client).await;
262 let header = CarHeader::new_v1(vec![]);
263 let header_cbor = header.encode().unwrap_or_default();
264 let mut car = Vec::new();
265 write_varint(&mut car, header_cbor.len() as u64);
266 car.extend_from_slice(&header_cbor);
267 let res = client
268 .post(format!(
269 "{}/xrpc/com.atproto.repo.importRepo",
270 base_url().await
271 ))
272 .bearer_auth(&token)
273 .header("Content-Type", "application/vnd.ipld.car")
274 .body(car)
275 .send()
276 .await
277 .expect("Request failed");
278 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
279 let body: serde_json::Value = res.json().await.unwrap();
280 assert_eq!(body["error"], "InvalidRequest");
281}
282
283#[tokio::test]
284async fn test_import_preserves_records_after_reimport() {
285 let client = client();
286 let (token, did) = create_account_and_login(&client).await;
287 let mut rkeys = Vec::new();
288 for i in 0..3 {
289 let post_payload = json!({
290 "repo": did,
291 "collection": "app.bsky.feed.post",
292 "record": {
293 "$type": "app.bsky.feed.post",
294 "text": format!("Test post {}", i),
295 "createdAt": chrono::Utc::now().to_rfc3339(),
296 }
297 });
298 let res = client
299 .post(format!(
300 "{}/xrpc/com.atproto.repo.createRecord",
301 base_url().await
302 ))
303 .bearer_auth(&token)
304 .json(&post_payload)
305 .send()
306 .await
307 .expect("Failed to create post");
308 assert_eq!(res.status(), StatusCode::OK);
309 let body: serde_json::Value = res.json().await.unwrap();
310 let uri = body["uri"].as_str().unwrap();
311 let rkey = uri.split('/').last().unwrap().to_string();
312 rkeys.push(rkey);
313 }
314 for rkey in &rkeys {
315 let get_res = client
316 .get(format!(
317 "{}/xrpc/com.atproto.repo.getRecord?repo={}&collection=app.bsky.feed.post&rkey={}",
318 base_url().await,
319 did,
320 rkey
321 ))
322 .send()
323 .await
324 .expect("Failed to get record before export");
325 assert_eq!(
326 get_res.status(),
327 StatusCode::OK,
328 "Record {} not found before export",
329 rkey
330 );
331 }
332 let export_res = client
333 .get(format!(
334 "{}/xrpc/com.atproto.sync.getRepo?did={}",
335 base_url().await,
336 did
337 ))
338 .send()
339 .await
340 .expect("Failed to export repo");
341 assert_eq!(export_res.status(), StatusCode::OK);
342 let car_bytes = export_res.bytes().await.unwrap();
343 let import_res = client
344 .post(format!(
345 "{}/xrpc/com.atproto.repo.importRepo",
346 base_url().await
347 ))
348 .bearer_auth(&token)
349 .header("Content-Type", "application/vnd.ipld.car")
350 .body(car_bytes.to_vec())
351 .send()
352 .await
353 .expect("Failed to import repo");
354 assert_eq!(import_res.status(), StatusCode::OK);
355 let list_res = client
356 .get(format!(
357 "{}/xrpc/com.atproto.repo.listRecords?repo={}&collection=app.bsky.feed.post",
358 base_url().await,
359 did
360 ))
361 .send()
362 .await
363 .expect("Failed to list records after import");
364 assert_eq!(list_res.status(), StatusCode::OK);
365 let list_body: serde_json::Value = list_res.json().await.unwrap();
366 let records_after = list_body["records"]
367 .as_array()
368 .map(|a| a.len())
369 .unwrap_or(0);
370 assert!(
371 records_after >= 1,
372 "Expected at least 1 record after import, found {}. Note: MST walk may have timing issues.",
373 records_after
374 );
375}