1use baseid_core::error::ProtocolError;
21use std::collections::HashMap;
22
23#[derive(Debug, Clone)]
25pub struct HttpResponse {
26 pub status: u16,
28 pub headers: HashMap<String, String>,
30 pub body: Vec<u8>,
32}
33
34impl HttpResponse {
35 pub fn json(&self) -> baseid_core::Result<serde_json::Value> {
37 serde_json::from_slice(&self.body).map_err(|_| ProtocolError::InvalidResponse.into())
38 }
39
40 pub fn text(&self) -> baseid_core::Result<String> {
42 String::from_utf8(self.body.clone()).map_err(|_| ProtocolError::InvalidResponse.into())
43 }
44
45 pub fn is_success(&self) -> bool {
47 (200..300).contains(&self.status)
48 }
49}
50
51#[allow(async_fn_in_trait)]
56pub trait HttpClient: Send + Sync {
57 async fn get_json(&self, url: &str) -> baseid_core::Result<serde_json::Value>;
59
60 async fn post_form(
62 &self,
63 url: &str,
64 params: &[(&str, &str)],
65 ) -> baseid_core::Result<serde_json::Value>;
66
67 async fn post_json_bearer(
69 &self,
70 url: &str,
71 body: &serde_json::Value,
72 token: &str,
73 ) -> baseid_core::Result<serde_json::Value>;
74
75 async fn post_json(
77 &self,
78 url: &str,
79 body: &serde_json::Value,
80 ) -> baseid_core::Result<serde_json::Value>;
81
82 async fn get(&self, url: &str) -> baseid_core::Result<HttpResponse>;
84
85 async fn post_raw(
87 &self,
88 url: &str,
89 body: &[u8],
90 content_type: &str,
91 ) -> baseid_core::Result<HttpResponse>;
92}
93
94#[cfg(feature = "http-reqwest")]
100pub struct ReqwestHttpClient {
101 inner: reqwest::Client,
102}
103
104#[cfg(feature = "http-reqwest")]
105impl ReqwestHttpClient {
106 pub fn new() -> Self {
108 Self {
109 inner: reqwest::Client::new(),
110 }
111 }
112
113 pub fn with_client(client: reqwest::Client) -> Self {
115 Self { inner: client }
116 }
117}
118
119#[cfg(feature = "http-reqwest")]
120impl Default for ReqwestHttpClient {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126#[cfg(feature = "http-reqwest")]
127impl HttpClient for ReqwestHttpClient {
128 async fn get_json(&self, url: &str) -> baseid_core::Result<serde_json::Value> {
129 let resp = self
130 .inner
131 .get(url)
132 .send()
133 .await
134 .map_err(|_| ProtocolError::Transport)?;
135 if !resp.status().is_success() {
136 return Err(ProtocolError::InvalidResponse.into());
137 }
138 resp.json()
139 .await
140 .map_err(|_| ProtocolError::InvalidResponse.into())
141 }
142
143 async fn post_form(
144 &self,
145 url: &str,
146 params: &[(&str, &str)],
147 ) -> baseid_core::Result<serde_json::Value> {
148 let resp = self
149 .inner
150 .post(url)
151 .form(params)
152 .send()
153 .await
154 .map_err(|_| ProtocolError::Transport)?;
155 if !resp.status().is_success() {
156 return Err(ProtocolError::InvalidResponse.into());
157 }
158 resp.json()
159 .await
160 .map_err(|_| ProtocolError::InvalidResponse.into())
161 }
162
163 async fn post_json_bearer(
164 &self,
165 url: &str,
166 body: &serde_json::Value,
167 token: &str,
168 ) -> baseid_core::Result<serde_json::Value> {
169 let resp = self
170 .inner
171 .post(url)
172 .bearer_auth(token)
173 .json(body)
174 .send()
175 .await
176 .map_err(|_| ProtocolError::Transport)?;
177 if !resp.status().is_success() {
178 return Err(ProtocolError::InvalidResponse.into());
179 }
180 resp.json()
181 .await
182 .map_err(|_| ProtocolError::InvalidResponse.into())
183 }
184
185 async fn post_json(
186 &self,
187 url: &str,
188 body: &serde_json::Value,
189 ) -> baseid_core::Result<serde_json::Value> {
190 let resp = self
191 .inner
192 .post(url)
193 .json(body)
194 .send()
195 .await
196 .map_err(|_| ProtocolError::Transport)?;
197 if !resp.status().is_success() {
198 return Err(ProtocolError::InvalidResponse.into());
199 }
200 resp.json()
201 .await
202 .map_err(|_| ProtocolError::InvalidResponse.into())
203 }
204
205 async fn get(&self, url: &str) -> baseid_core::Result<HttpResponse> {
206 let resp = self
207 .inner
208 .get(url)
209 .send()
210 .await
211 .map_err(|_| ProtocolError::Transport)?;
212 let status = resp.status().as_u16();
213 let headers = resp
214 .headers()
215 .iter()
216 .map(|(k, v)| {
217 (
218 k.as_str().to_lowercase(),
219 v.to_str().unwrap_or("").to_string(),
220 )
221 })
222 .collect();
223 let body = resp.bytes().await.map_err(|_| ProtocolError::Transport)?;
224 Ok(HttpResponse {
225 status,
226 headers,
227 body: body.to_vec(),
228 })
229 }
230
231 async fn post_raw(
232 &self,
233 url: &str,
234 body: &[u8],
235 content_type: &str,
236 ) -> baseid_core::Result<HttpResponse> {
237 let resp = self
238 .inner
239 .post(url)
240 .header("content-type", content_type)
241 .body(body.to_vec())
242 .send()
243 .await
244 .map_err(|_| ProtocolError::Transport)?;
245 let status = resp.status().as_u16();
246 let headers = resp
247 .headers()
248 .iter()
249 .map(|(k, v)| {
250 (
251 k.as_str().to_lowercase(),
252 v.to_str().unwrap_or("").to_string(),
253 )
254 })
255 .collect();
256 let body = resp.bytes().await.map_err(|_| ProtocolError::Transport)?;
257 Ok(HttpResponse {
258 status,
259 headers,
260 body: body.to_vec(),
261 })
262 }
263}
264
265pub struct MockHttpClient {
284 get_responses: std::sync::Mutex<HashMap<String, serde_json::Value>>,
285 post_responses: std::sync::Mutex<HashMap<String, serde_json::Value>>,
286 raw_get_responses: std::sync::Mutex<HashMap<String, HttpResponse>>,
287 raw_post_responses: std::sync::Mutex<HashMap<String, HttpResponse>>,
288}
289
290impl MockHttpClient {
291 pub fn new() -> Self {
293 Self {
294 get_responses: std::sync::Mutex::new(HashMap::new()),
295 post_responses: std::sync::Mutex::new(HashMap::new()),
296 raw_get_responses: std::sync::Mutex::new(HashMap::new()),
297 raw_post_responses: std::sync::Mutex::new(HashMap::new()),
298 }
299 }
300
301 pub fn on_get(self, url: &str, response: serde_json::Value) -> Self {
303 self.get_responses
304 .lock()
305 .unwrap()
306 .insert(url.to_string(), response);
307 self
308 }
309
310 pub fn on_post(self, url: &str, response: serde_json::Value) -> Self {
312 self.post_responses
313 .lock()
314 .unwrap()
315 .insert(url.to_string(), response);
316 self
317 }
318
319 pub fn on_get_raw(self, url: &str, response: HttpResponse) -> Self {
321 self.raw_get_responses
322 .lock()
323 .unwrap()
324 .insert(url.to_string(), response);
325 self
326 }
327
328 pub fn on_post_raw(self, url: &str, response: HttpResponse) -> Self {
330 self.raw_post_responses
331 .lock()
332 .unwrap()
333 .insert(url.to_string(), response);
334 self
335 }
336
337 fn get_json_response(&self, url: &str) -> baseid_core::Result<serde_json::Value> {
338 self.get_responses
339 .lock()
340 .unwrap()
341 .get(url)
342 .cloned()
343 .ok_or_else(|| ProtocolError::Transport.into())
344 }
345
346 fn get_post_response(&self, url: &str) -> baseid_core::Result<serde_json::Value> {
347 self.post_responses
348 .lock()
349 .unwrap()
350 .get(url)
351 .cloned()
352 .ok_or_else(|| ProtocolError::Transport.into())
353 }
354}
355
356impl Default for MockHttpClient {
357 fn default() -> Self {
358 Self::new()
359 }
360}
361
362impl HttpClient for MockHttpClient {
363 async fn get_json(&self, url: &str) -> baseid_core::Result<serde_json::Value> {
364 self.get_json_response(url)
365 }
366
367 async fn post_form(
368 &self,
369 url: &str,
370 _params: &[(&str, &str)],
371 ) -> baseid_core::Result<serde_json::Value> {
372 self.get_post_response(url)
373 }
374
375 async fn post_json_bearer(
376 &self,
377 url: &str,
378 _body: &serde_json::Value,
379 _token: &str,
380 ) -> baseid_core::Result<serde_json::Value> {
381 self.get_post_response(url)
382 }
383
384 async fn post_json(
385 &self,
386 url: &str,
387 _body: &serde_json::Value,
388 ) -> baseid_core::Result<serde_json::Value> {
389 self.get_post_response(url)
390 }
391
392 async fn get(&self, url: &str) -> baseid_core::Result<HttpResponse> {
393 self.raw_get_responses
394 .lock()
395 .unwrap()
396 .get(url)
397 .cloned()
398 .ok_or_else(|| ProtocolError::Transport.into())
399 }
400
401 async fn post_raw(
402 &self,
403 url: &str,
404 _body: &[u8],
405 _content_type: &str,
406 ) -> baseid_core::Result<HttpResponse> {
407 self.raw_post_responses
408 .lock()
409 .unwrap()
410 .get(url)
411 .cloned()
412 .ok_or_else(|| ProtocolError::Transport.into())
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use serde_json::json;
420
421 #[tokio::test]
422 async fn mock_get_json_returns_configured_response() {
423 let client =
424 MockHttpClient::new().on_get("https://example.com/metadata", json!({"issuer": "test"}));
425 let resp = client
426 .get_json("https://example.com/metadata")
427 .await
428 .unwrap();
429 assert_eq!(resp["issuer"], "test");
430 }
431
432 #[tokio::test]
433 async fn mock_get_json_returns_error_for_unknown_url() {
434 let client = MockHttpClient::new();
435 let result = client.get_json("https://unknown.example.com").await;
436 assert!(result.is_err());
437 }
438
439 #[tokio::test]
440 async fn mock_post_form_returns_configured_response() {
441 let client = MockHttpClient::new().on_post(
442 "https://example.com/token",
443 json!({"access_token": "tok_123", "token_type": "bearer"}),
444 );
445 let resp = client
446 .post_form(
447 "https://example.com/token",
448 &[("grant_type", "authorization_code")],
449 )
450 .await
451 .unwrap();
452 assert_eq!(resp["access_token"], "tok_123");
453 }
454
455 #[tokio::test]
456 async fn mock_post_json_bearer_returns_configured_response() {
457 let client = MockHttpClient::new().on_post(
458 "https://example.com/credential",
459 json!({"credential": "ey..."}),
460 );
461 let body = json!({"types": ["VerifiableCredential"]});
462 let resp = client
463 .post_json_bearer("https://example.com/credential", &body, "tok_123")
464 .await
465 .unwrap();
466 assert_eq!(resp["credential"], "ey...");
467 }
468
469 #[tokio::test]
470 async fn mock_post_json_returns_configured_response() {
471 let client =
472 MockHttpClient::new().on_post("https://example.com/present", json!({"status": "ok"}));
473 let body = json!({"vp_token": "ey..."});
474 let resp = client
475 .post_json("https://example.com/present", &body)
476 .await
477 .unwrap();
478 assert_eq!(resp["status"], "ok");
479 }
480
481 #[tokio::test]
482 async fn mock_post_returns_error_for_unknown_url() {
483 let client = MockHttpClient::new();
484 let result = client.post_form("https://unknown.example.com", &[]).await;
485 assert!(result.is_err());
486 }
487
488 #[tokio::test]
489 async fn mock_raw_get_returns_configured_response() {
490 let client = MockHttpClient::new().on_get_raw(
491 "https://example.com/doc",
492 HttpResponse {
493 status: 200,
494 headers: HashMap::new(),
495 body: b"hello".to_vec(),
496 },
497 );
498 let resp = client.get("https://example.com/doc").await.unwrap();
499 assert_eq!(resp.status, 200);
500 assert_eq!(resp.text().unwrap(), "hello");
501 }
502
503 #[tokio::test]
504 async fn mock_raw_post_returns_configured_response() {
505 let client = MockHttpClient::new().on_post_raw(
506 "https://example.com/submit",
507 HttpResponse {
508 status: 201,
509 headers: HashMap::new(),
510 body: b"{\"id\":\"abc\"}".to_vec(),
511 },
512 );
513 let resp = client
514 .post_raw("https://example.com/submit", b"data", "application/json")
515 .await
516 .unwrap();
517 assert_eq!(resp.status, 201);
518 assert_eq!(resp.json().unwrap()["id"], "abc");
519 }
520
521 #[tokio::test]
522 async fn http_response_is_success() {
523 let ok = HttpResponse {
524 status: 200,
525 headers: HashMap::new(),
526 body: vec![],
527 };
528 assert!(ok.is_success());
529 let created = HttpResponse {
530 status: 201,
531 headers: HashMap::new(),
532 body: vec![],
533 };
534 assert!(created.is_success());
535 let not_found = HttpResponse {
536 status: 404,
537 headers: HashMap::new(),
538 body: vec![],
539 };
540 assert!(!not_found.is_success());
541 let error = HttpResponse {
542 status: 500,
543 headers: HashMap::new(),
544 body: vec![],
545 };
546 assert!(!error.is_success());
547 }
548
549 #[tokio::test]
550 async fn http_response_json_parsing() {
551 let resp = HttpResponse {
552 status: 200,
553 headers: HashMap::new(),
554 body: b"{\"key\":\"value\"}".to_vec(),
555 };
556 let json = resp.json().unwrap();
557 assert_eq!(json["key"], "value");
558 }
559
560 #[tokio::test]
561 async fn http_response_json_parse_error() {
562 let resp = HttpResponse {
563 status: 200,
564 headers: HashMap::new(),
565 body: b"not json".to_vec(),
566 };
567 assert!(resp.json().is_err());
568 }
569
570 #[tokio::test]
571 async fn mock_client_is_send_sync() {
572 fn assert_send_sync<T: Send + Sync>() {}
573 assert_send_sync::<MockHttpClient>();
574 }
575
576 #[tokio::test]
577 async fn mock_multiple_urls() {
578 let client = MockHttpClient::new()
579 .on_get("https://a.example.com", json!({"site": "a"}))
580 .on_get("https://b.example.com", json!({"site": "b"}))
581 .on_post("https://c.example.com", json!({"site": "c"}));
582
583 assert_eq!(
584 client.get_json("https://a.example.com").await.unwrap()["site"],
585 "a"
586 );
587 assert_eq!(
588 client.get_json("https://b.example.com").await.unwrap()["site"],
589 "b"
590 );
591 assert_eq!(
592 client
593 .post_form("https://c.example.com", &[])
594 .await
595 .unwrap()["site"],
596 "c"
597 );
598 }
599
600 #[cfg(feature = "http-reqwest")]
601 #[test]
602 fn reqwest_client_is_send_sync() {
603 fn assert_send_sync<T: Send + Sync>() {}
604 assert_send_sync::<ReqwestHttpClient>();
605 }
606}