baseid_transport/
http.rs

1//! HTTP client abstraction for BaseID protocol flows.
2//!
3//! Provides an injectable async HTTP client trait used by OID4VCI, OID4VP,
4//! SIOPv2, DID resolution, and DIDComm message delivery.
5//!
6//! # Usage
7//!
8//! Protocol crates accept `impl HttpClient` so tests can inject `MockHttpClient`
9//! instead of making real network requests.
10//!
11//! ```rust
12//! use baseid_transport::HttpClient;
13//!
14//! async fn discover_issuer(client: &impl HttpClient, url: &str) {
15//!     let metadata = client.get_json(url).await.unwrap();
16//!     // ...
17//! }
18//! ```
19
20use baseid_core::error::ProtocolError;
21use std::collections::HashMap;
22
23/// HTTP response with status, headers, and body.
24#[derive(Debug, Clone)]
25pub struct HttpResponse {
26    /// HTTP status code.
27    pub status: u16,
28    /// Response headers (lowercase keys).
29    pub headers: HashMap<String, String>,
30    /// Response body as bytes.
31    pub body: Vec<u8>,
32}
33
34impl HttpResponse {
35    /// Parse the body as JSON.
36    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    /// Return the body as a UTF-8 string.
41    pub fn text(&self) -> baseid_core::Result<String> {
42        String::from_utf8(self.body.clone()).map_err(|_| ProtocolError::InvalidResponse.into())
43    }
44
45    /// Whether the status code indicates success (2xx).
46    pub fn is_success(&self) -> bool {
47        (200..300).contains(&self.status)
48    }
49}
50
51/// Async HTTP client trait for BaseID protocol network requests.
52///
53/// All protocol crates (OID4VCI, OID4VP, SIOPv2, DIDComm) accept this trait
54/// for dependency injection and testability.
55#[allow(async_fn_in_trait)]
56pub trait HttpClient: Send + Sync {
57    /// Perform a GET request and parse the JSON response.
58    async fn get_json(&self, url: &str) -> baseid_core::Result<serde_json::Value>;
59
60    /// Perform a POST request with form-encoded parameters and parse the JSON response.
61    async fn post_form(
62        &self,
63        url: &str,
64        params: &[(&str, &str)],
65    ) -> baseid_core::Result<serde_json::Value>;
66
67    /// Perform a POST request with a JSON body and bearer token, returning the JSON response.
68    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    /// Perform a POST request with a JSON body (no auth), returning the JSON response.
76    async fn post_json(
77        &self,
78        url: &str,
79        body: &serde_json::Value,
80    ) -> baseid_core::Result<serde_json::Value>;
81
82    /// Perform a raw GET request, returning the full response.
83    async fn get(&self, url: &str) -> baseid_core::Result<HttpResponse>;
84
85    /// Perform a raw POST request with bytes body and content type.
86    async fn post_raw(
87        &self,
88        url: &str,
89        body: &[u8],
90        content_type: &str,
91    ) -> baseid_core::Result<HttpResponse>;
92}
93
94// ---------------------------------------------------------------------------
95// ReqwestHttpClient — default production backend
96// ---------------------------------------------------------------------------
97
98/// HTTP client backed by `reqwest` (enabled by default via the `http-reqwest` feature).
99#[cfg(feature = "http-reqwest")]
100pub struct ReqwestHttpClient {
101    inner: reqwest::Client,
102}
103
104#[cfg(feature = "http-reqwest")]
105impl ReqwestHttpClient {
106    /// Create a new client with default settings.
107    pub fn new() -> Self {
108        Self {
109            inner: reqwest::Client::new(),
110        }
111    }
112
113    /// Create a client wrapping an existing `reqwest::Client`.
114    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
265// ---------------------------------------------------------------------------
266// MockHttpClient — for testing without network access
267// ---------------------------------------------------------------------------
268
269/// Mock HTTP client for testing. Returns pre-configured responses.
270///
271/// # Example
272///
273/// ```rust
274/// use baseid_transport::MockHttpClient;
275/// use serde_json::json;
276///
277/// let mock = MockHttpClient::new()
278///     .on_get("https://issuer.example.com/.well-known/openid-credential-issuer",
279///         json!({"credential_issuer": "https://issuer.example.com"}))
280///     .on_post("https://issuer.example.com/token",
281///         json!({"access_token": "tok_123", "token_type": "bearer"}));
282/// ```
283pub 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    /// Create a new empty mock client.
292    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    /// Register a JSON response for a GET URL.
302    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    /// Register a JSON response for a POST URL.
311    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    /// Register a raw HTTP response for a GET URL.
320    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    /// Register a raw HTTP response for a POST URL.
329    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}