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!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
12 .header("Content-Type", "application/vnd.ipld.car")
13 .body(vec![0u8; 100])
14 .send()
15 .await
16 .expect("Request failed");
17 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
18}
19
20#[tokio::test]
21async fn test_import_repo_invalid_car() {
22 let client = client();
23 let (token, _did) = create_account_and_login(&client).await;
24 let res = client
25 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
26 .bearer_auth(&token)
27 .header("Content-Type", "application/vnd.ipld.car")
28 .body(vec![0u8; 100])
29 .send()
30 .await
31 .expect("Request failed");
32 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
33 let body: serde_json::Value = res.json().await.unwrap();
34 assert_eq!(body["error"], "InvalidRequest");
35}
36
37#[tokio::test]
38async fn test_import_repo_empty_body() {
39 let client = client();
40 let (token, _did) = create_account_and_login(&client).await;
41 let res = client
42 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
43 .bearer_auth(&token)
44 .header("Content-Type", "application/vnd.ipld.car")
45 .body(vec![])
46 .send()
47 .await
48 .expect("Request failed");
49 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
50}
51
52fn write_varint(buf: &mut Vec<u8>, mut value: u64) {
53 loop {
54 let mut byte = (value & 0x7F) as u8;
55 value >>= 7;
56 if value != 0 {
57 byte |= 0x80;
58 }
59 buf.push(byte);
60 if value == 0 {
61 break;
62 }
63 }
64}
65
66#[tokio::test]
67async fn test_import_rejects_car_for_different_user() {
68 let client = client();
69 let (token_a, _did_a) = create_account_and_login(&client).await;
70 let (_token_b, did_b) = create_account_and_login(&client).await;
71 let export_res = client
72 .get(format!(
73 "{}/xrpc/com.atproto.sync.getRepo?did={}",
74 base_url().await,
75 did_b
76 ))
77 .send()
78 .await
79 .expect("Export failed");
80 assert_eq!(export_res.status(), StatusCode::OK);
81 let car_bytes = export_res.bytes().await.unwrap();
82 let import_res = client
83 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
84 .bearer_auth(&token_a)
85 .header("Content-Type", "application/vnd.ipld.car")
86 .body(car_bytes.to_vec())
87 .send()
88 .await
89 .expect("Import failed");
90 assert_eq!(import_res.status(), StatusCode::FORBIDDEN);
91 let body: serde_json::Value = import_res.json().await.unwrap();
92 assert!(
93 body["error"] == "InvalidRequest" || body["error"] == "DidMismatch",
94 "Expected DidMismatch or InvalidRequest error, got: {:?}",
95 body
96 );
97}
98
99#[tokio::test]
100async fn test_import_accepts_own_exported_repo() {
101 let client = client();
102 let (token, did) = create_account_and_login(&client).await;
103 let post_payload = json!({
104 "repo": did,
105 "collection": "app.bsky.feed.post",
106 "record": {
107 "$type": "app.bsky.feed.post",
108 "text": "Original post before export",
109 "createdAt": chrono::Utc::now().to_rfc3339(),
110 }
111 });
112 let create_res = client
113 .post(format!(
114 "{}/xrpc/com.atproto.repo.createRecord",
115 base_url().await
116 ))
117 .bearer_auth(&token)
118 .json(&post_payload)
119 .send()
120 .await
121 .expect("Failed to create post");
122 assert_eq!(create_res.status(), StatusCode::OK);
123 let export_res = client
124 .get(format!(
125 "{}/xrpc/com.atproto.sync.getRepo?did={}",
126 base_url().await,
127 did
128 ))
129 .send()
130 .await
131 .expect("Failed to export repo");
132 assert_eq!(export_res.status(), StatusCode::OK);
133 let car_bytes = export_res.bytes().await.unwrap();
134 let import_res = client
135 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
136 .bearer_auth(&token)
137 .header("Content-Type", "application/vnd.ipld.car")
138 .body(car_bytes.to_vec())
139 .send()
140 .await
141 .expect("Failed to import repo");
142 assert_eq!(import_res.status(), StatusCode::OK);
143}
144
145#[tokio::test]
146async fn test_import_repo_size_limit() {
147 let client = client();
148 let (token, _did) = create_account_and_login(&client).await;
149 let oversized_body = vec![0u8; 110 * 1024 * 1024];
150 let 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(oversized_body)
155 .send()
156 .await;
157 match res {
158 Ok(response) => {
159 assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
160 }
161 Err(e) => {
162 let error_str = e.to_string().to_lowercase();
163 assert!(
164 error_str.contains("broken pipe") ||
165 error_str.contains("connection") ||
166 error_str.contains("reset") ||
167 error_str.contains("request") ||
168 error_str.contains("body"),
169 "Expected connection error or PAYLOAD_TOO_LARGE, got: {}",
170 e
171 );
172 }
173 }
174}
175
176#[tokio::test]
177async fn test_import_deactivated_account_rejected() {
178 let client = client();
179 let (token, did) = create_account_and_login(&client).await;
180 let export_res = client
181 .get(format!(
182 "{}/xrpc/com.atproto.sync.getRepo?did={}",
183 base_url().await,
184 did
185 ))
186 .send()
187 .await
188 .expect("Export failed");
189 assert_eq!(export_res.status(), StatusCode::OK);
190 let car_bytes = export_res.bytes().await.unwrap();
191 let deactivate_res = client
192 .post(format!(
193 "{}/xrpc/com.atproto.server.deactivateAccount",
194 base_url().await
195 ))
196 .bearer_auth(&token)
197 .json(&json!({}))
198 .send()
199 .await
200 .expect("Deactivate failed");
201 assert!(deactivate_res.status().is_success());
202 let import_res = client
203 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
204 .bearer_auth(&token)
205 .header("Content-Type", "application/vnd.ipld.car")
206 .body(car_bytes.to_vec())
207 .send()
208 .await
209 .expect("Import failed");
210 assert!(
211 import_res.status() == StatusCode::FORBIDDEN || import_res.status() == StatusCode::UNAUTHORIZED,
212 "Expected FORBIDDEN (403) or UNAUTHORIZED (401), got {}",
213 import_res.status()
214 );
215}
216
217#[tokio::test]
218async fn test_import_invalid_car_structure() {
219 let client = client();
220 let (token, _did) = create_account_and_login(&client).await;
221 let invalid_car = vec![0x0a, 0xa1, 0x65, 0x72, 0x6f, 0x6f, 0x74, 0x73, 0x80];
222 let res = client
223 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
224 .bearer_auth(&token)
225 .header("Content-Type", "application/vnd.ipld.car")
226 .body(invalid_car)
227 .send()
228 .await
229 .expect("Request failed");
230 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
231}
232
233#[tokio::test]
234async fn test_import_car_with_no_roots() {
235 let client = client();
236 let (token, _did) = create_account_and_login(&client).await;
237 let header = CarHeader::new_v1(vec![]);
238 let header_cbor = header.encode().unwrap_or_default();
239 let mut car = Vec::new();
240 write_varint(&mut car, header_cbor.len() as u64);
241 car.extend_from_slice(&header_cbor);
242 let res = client
243 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
244 .bearer_auth(&token)
245 .header("Content-Type", "application/vnd.ipld.car")
246 .body(car)
247 .send()
248 .await
249 .expect("Request failed");
250 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
251 let body: serde_json::Value = res.json().await.unwrap();
252 assert_eq!(body["error"], "InvalidRequest");
253}
254
255#[tokio::test]
256async fn test_import_preserves_records_after_reimport() {
257 let client = client();
258 let (token, did) = create_account_and_login(&client).await;
259 let mut rkeys = Vec::new();
260 for i in 0..3 {
261 let post_payload = json!({
262 "repo": did,
263 "collection": "app.bsky.feed.post",
264 "record": {
265 "$type": "app.bsky.feed.post",
266 "text": format!("Test post {}", i),
267 "createdAt": chrono::Utc::now().to_rfc3339(),
268 }
269 });
270 let res = client
271 .post(format!(
272 "{}/xrpc/com.atproto.repo.createRecord",
273 base_url().await
274 ))
275 .bearer_auth(&token)
276 .json(&post_payload)
277 .send()
278 .await
279 .expect("Failed to create post");
280 assert_eq!(res.status(), StatusCode::OK);
281 let body: serde_json::Value = res.json().await.unwrap();
282 let uri = body["uri"].as_str().unwrap();
283 let rkey = uri.split('/').last().unwrap().to_string();
284 rkeys.push(rkey);
285 }
286 for rkey in &rkeys {
287 let get_res = client
288 .get(format!(
289 "{}/xrpc/com.atproto.repo.getRecord?repo={}&collection=app.bsky.feed.post&rkey={}",
290 base_url().await,
291 did,
292 rkey
293 ))
294 .send()
295 .await
296 .expect("Failed to get record before export");
297 assert_eq!(get_res.status(), StatusCode::OK, "Record {} not found before export", rkey);
298 }
299 let export_res = client
300 .get(format!(
301 "{}/xrpc/com.atproto.sync.getRepo?did={}",
302 base_url().await,
303 did
304 ))
305 .send()
306 .await
307 .expect("Failed to export repo");
308 assert_eq!(export_res.status(), StatusCode::OK);
309 let car_bytes = export_res.bytes().await.unwrap();
310 let import_res = client
311 .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
312 .bearer_auth(&token)
313 .header("Content-Type", "application/vnd.ipld.car")
314 .body(car_bytes.to_vec())
315 .send()
316 .await
317 .expect("Failed to import repo");
318 assert_eq!(import_res.status(), StatusCode::OK);
319 let list_res = client
320 .get(format!(
321 "{}/xrpc/com.atproto.repo.listRecords?repo={}&collection=app.bsky.feed.post",
322 base_url().await,
323 did
324 ))
325 .send()
326 .await
327 .expect("Failed to list records after import");
328 assert_eq!(list_res.status(), StatusCode::OK);
329 let list_body: serde_json::Value = list_res.json().await.unwrap();
330 let records_after = list_body["records"].as_array().map(|a| a.len()).unwrap_or(0);
331 assert!(
332 records_after >= 1,
333 "Expected at least 1 record after import, found {}. Note: MST walk may have timing issues.",
334 records_after
335 );
336}