this repo has no description
1mod common;
2use common::*;
3
4use iroh_car::CarHeader;
5use reqwest::StatusCode;
6use serde_json::json;
7
8#[tokio::test]
9async fn test_import_repo_requires_auth() {
10 let client = client();
11
12 let res = client
13 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
14 .header("Content-Type", "application/vnd.ipld.car")
15 .body(vec![0u8; 100])
16 .send()
17 .await
18 .expect("Request failed");
19
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
28 let res = client
29 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
30 .bearer_auth(&token)
31 .header("Content-Type", "application/vnd.ipld.car")
32 .body(vec![0u8; 100])
33 .send()
34 .await
35 .expect("Request failed");
36
37 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
38 let body: serde_json::Value = res.json().await.unwrap();
39 assert_eq!(body["error"], "InvalidRequest");
40}
41
42#[tokio::test]
43async fn test_import_repo_empty_body() {
44 let client = client();
45 let (token, _did) = create_account_and_login(&client).await;
46
47 let res = client
48 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
49 .bearer_auth(&token)
50 .header("Content-Type", "application/vnd.ipld.car")
51 .body(vec![])
52 .send()
53 .await
54 .expect("Request failed");
55
56 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
57}
58
59fn write_varint(buf: &mut Vec<u8>, mut value: u64) {
60 loop {
61 let mut byte = (value & 0x7F) as u8;
62 value >>= 7;
63 if value != 0 {
64 byte |= 0x80;
65 }
66 buf.push(byte);
67 if value == 0 {
68 break;
69 }
70 }
71}
72
73#[tokio::test]
74async fn test_import_rejects_car_for_different_user() {
75 let client = client();
76
77 let (token_a, _did_a) = create_account_and_login(&client).await;
78 let (_token_b, did_b) = create_account_and_login(&client).await;
79
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
90 assert_eq!(export_res.status(), StatusCode::OK);
91 let car_bytes = export_res.bytes().await.unwrap();
92
93 let import_res = client
94 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
95 .bearer_auth(&token_a)
96 .header("Content-Type", "application/vnd.ipld.car")
97 .body(car_bytes.to_vec())
98 .send()
99 .await
100 .expect("Import failed");
101
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
116 let post_payload = json!({
117 "repo": did,
118 "collection": "app.bsky.feed.post",
119 "record": {
120 "$type": "app.bsky.feed.post",
121 "text": "Original post before export",
122 "createdAt": chrono::Utc::now().to_rfc3339(),
123 }
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
138 let export_res = client
139 .get(format!(
140 "{}/xrpc/com.atproto.sync.getRepo?did={}",
141 base_url().await,
142 did
143 ))
144 .send()
145 .await
146 .expect("Failed to export repo");
147 assert_eq!(export_res.status(), StatusCode::OK);
148 let car_bytes = export_res.bytes().await.unwrap();
149
150 let import_res = client
151 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
152 .bearer_auth(&token)
153 .header("Content-Type", "application/vnd.ipld.car")
154 .body(car_bytes.to_vec())
155 .send()
156 .await
157 .expect("Failed to import repo");
158
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
167 let oversized_body = vec![0u8; 110 * 1024 * 1024];
168
169 let res = client
170 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
171 .bearer_auth(&token)
172 .header("Content-Type", "application/vnd.ipld.car")
173 .body(oversized_body)
174 .send()
175 .await;
176
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_rejected() {
198 let client = client();
199 let (token, did) = create_account_and_login(&client).await;
200
201 let export_res = client
202 .get(format!(
203 "{}/xrpc/com.atproto.sync.getRepo?did={}",
204 base_url().await,
205 did
206 ))
207 .send()
208 .await
209 .expect("Export failed");
210 assert_eq!(export_res.status(), StatusCode::OK);
211 let car_bytes = export_res.bytes().await.unwrap();
212
213 let deactivate_res = client
214 .post(format!(
215 "{}/xrpc/com.atproto.server.deactivateAccount",
216 base_url().await
217 ))
218 .bearer_auth(&token)
219 .json(&json!({}))
220 .send()
221 .await
222 .expect("Deactivate failed");
223 assert!(deactivate_res.status().is_success());
224
225 let import_res = client
226 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
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
234 assert!(
235 import_res.status() == StatusCode::FORBIDDEN || import_res.status() == StatusCode::UNAUTHORIZED,
236 "Expected FORBIDDEN (403) or UNAUTHORIZED (401), got {}",
237 import_res.status()
238 );
239}
240
241#[tokio::test]
242async fn test_import_invalid_car_structure() {
243 let client = client();
244 let (token, _did) = create_account_and_login(&client).await;
245
246 let invalid_car = vec![0x0a, 0xa1, 0x65, 0x72, 0x6f, 0x6f, 0x74, 0x73, 0x80];
247
248 let res = client
249 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
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
257 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
258}
259
260#[tokio::test]
261async fn test_import_car_with_no_roots() {
262 let client = client();
263 let (token, _did) = create_account_and_login(&client).await;
264
265 let header = CarHeader::new_v1(vec![]);
266 let header_cbor = header.encode().unwrap_or_default();
267 let mut car = Vec::new();
268 write_varint(&mut car, header_cbor.len() as u64);
269 car.extend_from_slice(&header_cbor);
270
271 let res = client
272 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
273 .bearer_auth(&token)
274 .header("Content-Type", "application/vnd.ipld.car")
275 .body(car)
276 .send()
277 .await
278 .expect("Request failed");
279
280 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
281 let body: serde_json::Value = res.json().await.unwrap();
282 assert_eq!(body["error"], "InvalidRequest");
283}
284
285#[tokio::test]
286async fn test_import_preserves_records_after_reimport() {
287 let client = client();
288 let (token, did) = create_account_and_login(&client).await;
289
290 let mut rkeys = Vec::new();
291 for i in 0..3 {
292 let post_payload = json!({
293 "repo": did,
294 "collection": "app.bsky.feed.post",
295 "record": {
296 "$type": "app.bsky.feed.post",
297 "text": format!("Test post {}", i),
298 "createdAt": chrono::Utc::now().to_rfc3339(),
299 }
300 });
301
302 let res = client
303 .post(format!(
304 "{}/xrpc/com.atproto.repo.createRecord",
305 base_url().await
306 ))
307 .bearer_auth(&token)
308 .json(&post_payload)
309 .send()
310 .await
311 .expect("Failed to create post");
312 assert_eq!(res.status(), StatusCode::OK);
313
314 let body: serde_json::Value = res.json().await.unwrap();
315 let uri = body["uri"].as_str().unwrap();
316 let rkey = uri.split('/').last().unwrap().to_string();
317 rkeys.push(rkey);
318 }
319
320 for rkey in &rkeys {
321 let get_res = client
322 .get(format!(
323 "{}/xrpc/com.atproto.repo.getRecord?repo={}&collection=app.bsky.feed.post&rkey={}",
324 base_url().await,
325 did,
326 rkey
327 ))
328 .send()
329 .await
330 .expect("Failed to get record before export");
331 assert_eq!(get_res.status(), StatusCode::OK, "Record {} not found before export", rkey);
332 }
333
334 let export_res = client
335 .get(format!(
336 "{}/xrpc/com.atproto.sync.getRepo?did={}",
337 base_url().await,
338 did
339 ))
340 .send()
341 .await
342 .expect("Failed to export repo");
343 assert_eq!(export_res.status(), StatusCode::OK);
344 let car_bytes = export_res.bytes().await.unwrap();
345
346 let import_res = client
347 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
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
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"].as_array().map(|a| a.len()).unwrap_or(0);
368
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}