borderless/
http.rs

1//! Definition of generic models used throughout different APIs
2
3use std::str::FromStr;
4
5use borderless_id_types::{AgentId, TxIdentifier};
6use http::header::CONTENT_TYPE;
7use queries::Pagination;
8use serde::de::DeserializeOwned;
9use serde::Serialize;
10
11use crate::__private::send_http_rq;
12use crate::common::{Description, Metadata};
13use crate::contracts::Info;
14use crate::events::{CallAction, Sink};
15use crate::warn;
16
17pub use http::{HeaderName, HeaderValue, Method, Request, Response, StatusCode, Version};
18
19/// Special trait that bundles the serialization of a type into a request body together with the corresponding content-type
20///
21/// For empty bodies with no content type `None` should be returned.
22pub trait IntoBodyAndContentType {
23    fn into_parts(self) -> anyhow::Result<Option<(Vec<u8>, &'static str)>>;
24}
25
26/// Wrapper type that automatically serializes the inner value to JSON and sets the content-type to `application/json`, if used together with a request.
27///
28/// See [`send_request`] function.
29pub struct Json<T>(pub T);
30
31impl<T: Serialize> IntoBodyAndContentType for Json<T> {
32    fn into_parts(self) -> anyhow::Result<Option<(Vec<u8>, &'static str)>> {
33        let body = serde_json::to_vec(&self.0)?;
34        Ok(Some((body, "application/json")))
35    }
36}
37
38/// Wrapper type that automatically serializes the inner value to plain text and sets the content-type to `text/plain`, if used together with a request.
39///
40/// See [`send_request`] function.
41pub struct Text<T>(pub T);
42
43impl<T: ToString> IntoBodyAndContentType for Text<T> {
44    fn into_parts(self) -> anyhow::Result<Option<(Vec<u8>, &'static str)>> {
45        let body = self.0.to_string().into_bytes();
46        Ok(Some((body, "text/plain")))
47    }
48}
49
50impl IntoBodyAndContentType for String {
51    fn into_parts(self) -> anyhow::Result<Option<(Vec<u8>, &'static str)>> {
52        let body = self.into_bytes();
53        Ok(Some((body, "text/plain")))
54    }
55}
56
57impl IntoBodyAndContentType for &str {
58    fn into_parts(self) -> anyhow::Result<Option<(Vec<u8>, &'static str)>> {
59        let body = self.to_string().into_bytes();
60        Ok(Some((body, "text/plain")))
61    }
62}
63
64/// Type that indicates an empty body. In this case no content-type header is set.
65///
66/// Identical to unit type `()`.
67///
68/// See [`send_request`] function.
69pub struct Empty;
70
71impl IntoBodyAndContentType for Empty {
72    fn into_parts(self) -> anyhow::Result<Option<(Vec<u8>, &'static str)>> {
73        Ok(None)
74    }
75}
76
77impl IntoBodyAndContentType for () {
78    fn into_parts(self) -> anyhow::Result<Option<(Vec<u8>, &'static str)>> {
79        Ok(None)
80    }
81}
82
83/// Wrapper type that automatically serializes the inner value to bytes and sets the content-type to `application/octet-stream`, if used together with a request.
84///
85/// See [`send_request`] function.
86pub struct Binary<T>(pub T);
87
88impl<T: Into<Vec<u8>>> IntoBodyAndContentType for Binary<T> {
89    fn into_parts(self) -> anyhow::Result<Option<(Vec<u8>, &'static str)>> {
90        let body = self.0.into();
91        Ok(Some((body, "application/octet-stream")))
92    }
93}
94
95impl IntoBodyAndContentType for Vec<u8> {
96    fn into_parts(self) -> anyhow::Result<Option<(Vec<u8>, &'static str)>> {
97        Ok(Some((self, "application/octet-stream")))
98    }
99}
100
101/// Simple function to perform a GET request
102pub fn get(url: impl AsRef<str>) -> anyhow::Result<Response<Vec<u8>>> {
103    let request = Request::builder()
104        .method(Method::GET)
105        .uri(url.as_ref())
106        .body(())?;
107    send_request(request)
108}
109
110/// Simple function to perform a POST request with some json payload
111pub fn post_json<T: Serialize, U: AsRef<str>>(
112    data: T,
113    url: U,
114) -> anyhow::Result<Response<Vec<u8>>> {
115    let request = Request::builder()
116        .method(Method::POST)
117        .uri(url.as_ref())
118        .body(Json(data))?;
119    send_request(request)
120}
121
122/// Send a http-request from webassembly and receive the response
123pub fn send_request<T>(request: Request<T>) -> anyhow::Result<Response<Vec<u8>>>
124where
125    T: IntoBodyAndContentType,
126{
127    let (mut parts, body) = request.into_parts();
128
129    // Inject correct content-type ( for empty bodies () we don't set the header value )
130    let body_bytes = match body.into_parts()? {
131        Some((bytes, content_type)) => {
132            parts
133                .headers
134                .insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
135            bytes
136        }
137        None => Vec::new(),
138    };
139
140    // Serialize request head according to protocol
141    let mut head = format!("{} {} {:?}\r\n", parts.method, parts.uri, parts.version);
142    for (name, value) in parts.headers.iter() {
143        head.push_str(&format!("{}: {}\r\n", name, value.to_str().unwrap()));
144    }
145    head.push_str("\r\n"); // End of headers
146
147    // Perform the ABI call to actually send the request
148    let (rs_head, rs_body) = send_http_rq(head, body_bytes).map_err(anyhow::Error::msg)?;
149    let rs = build_response_from_parts(&rs_head, rs_body)?;
150    Ok(rs)
151}
152
153/// Helper function to parse the body of a response into a deserializable value
154///
155/// Will generate a warning, if the content-type is not `application/json`
156pub fn as_json<T>(response: Response<Vec<u8>>) -> anyhow::Result<T>
157where
158    T: DeserializeOwned,
159{
160    if !response
161        .headers()
162        .get(CONTENT_TYPE)
163        .and_then(|h| h.to_str().ok())
164        .map(|h| h.starts_with("application/json"))
165        .unwrap_or_default()
166    {
167        warn!("as_json: Response does not have content-type 'application/json'");
168    }
169    Ok(serde_json::from_slice(response.body())?)
170}
171
172/// Helper function to parse the body of a response into a string
173///
174/// Will generate a warning, if the content-type is not `text/plain`
175pub fn as_text(response: Response<Vec<u8>>) -> anyhow::Result<String> {
176    if !response
177        .headers()
178        .get(CONTENT_TYPE)
179        .and_then(|h| h.to_str().ok())
180        .map(|h| h.starts_with("text/plain"))
181        .unwrap_or_default()
182    {
183        warn!("as_text: Response does not have content-type 'text/plain'");
184    }
185    Ok(String::from_utf8(response.into_body())?)
186}
187
188/// Helper function to parse a [`Response`] from the raw parts
189fn build_response_from_parts(head: &str, body: Vec<u8>) -> anyhow::Result<Response<Vec<u8>>> {
190    let mut lines = head.lines();
191
192    // Parse the status line
193    let status_line = lines
194        .next()
195        .ok_or_else(|| anyhow::anyhow!("Empty response head"))?;
196    let mut status_parts = status_line.splitn(3, ' ');
197
198    let version_str = status_parts
199        .next()
200        .ok_or_else(|| anyhow::anyhow!("Missing HTTP version"))?;
201    let status_code_str = status_parts
202        .next()
203        .ok_or_else(|| anyhow::anyhow!("Missing status code"))?;
204    let _reason_phrase = status_parts.next().unwrap_or(""); // Optional, ignore for now
205
206    let version = match version_str {
207        "HTTP/1.0" => Version::HTTP_10,
208        "HTTP/1.1" => Version::HTTP_11,
209        "HTTP/2.0" | "HTTP/2" => Version::HTTP_2,
210        _ => return Err(anyhow::anyhow!("Unsupported HTTP version: {}", version_str)),
211    };
212
213    let status_code = StatusCode::from_bytes(status_code_str.as_bytes())?;
214
215    // Build base response;
216    let mut response = Response::builder().status(status_code).version(version);
217
218    let headers = response.headers_mut().unwrap();
219
220    // Parse headers
221    for line in lines {
222        if line.trim().is_empty() {
223            continue; // Skip empty lines
224        }
225        if let Some((name, value)) = line.split_once(':') {
226            let header_name = HeaderName::from_str(name.trim())?;
227            let header_value = HeaderValue::from_str(value.trim())?;
228            headers.insert(header_name, header_value);
229        } else {
230            return Err(anyhow::anyhow!("Malformed header line: {}", line));
231        }
232    }
233    // Build the response
234    Ok(response.body(body)?)
235}
236
237/// Default return type for all routes that return lists.
238///
239/// Since we never want to have an infinitely large list returned from an endpoint,
240/// the number of entries an endpoint returns by default is limited.
241///
242/// However, the user must know, how many elements there are in total,
243/// as this information is crucial for building pagination elements in a frontend.
244///
245/// This type serves as a wrapper around `Vec<T>` (which will be serialized to a list in json),
246/// that also includes how many elements there are in total.
247#[derive(Serialize)]
248pub struct PaginatedElements<T>
249where
250    T: Serialize,
251{
252    pub elements: Vec<T>,
253    pub total_elements: usize,
254    #[serde(flatten)]
255    pub pagination: Pagination,
256}
257
258/// Wrapper to connect contract-actions with their tx-identifier and the related timestamp
259#[derive(Debug, Clone, Serialize)]
260pub struct TxAction {
261    /// Transaction identifier
262    pub tx_id: TxIdentifier,
263    /// Serializable action object
264    pub action: CallAction,
265    pub commited: u64,
266}
267
268/// Json description of a contract
269///
270/// Groups the most relevant information around a contract in a single datastructure.
271#[derive(Debug, Clone, Serialize)]
272pub struct ContractInfo {
273    pub info: Option<Info>,
274    pub desc: Option<Description>,
275    pub meta: Option<Metadata>,
276}
277
278/// Json description of an agent
279///
280/// Groups the most relevant information around a contract in a single datastructure.
281#[derive(Debug, Clone, Serialize)]
282pub struct AgentInfo {
283    pub agent_id: AgentId,
284    pub sinks: Vec<Sink>,
285    pub desc: Option<Description>,
286    pub meta: Option<Metadata>,
287}
288
289pub mod queries {
290    use std::{
291        collections::{HashMap, HashSet},
292        fmt::Display,
293        str::FromStr,
294    };
295
296    use borderless_id_types::{AgentId, ContractId};
297    use serde::Serialize;
298
299    pub struct Query {
300        /// Key-Value pairs of the query, where the key is one of the following keywords:
301        /// - page, per_page
302        /// - sort, order
303        /// - action
304        ///
305        /// These are handled seperately, because we my build a [`Pagination`] or [`Sorting`] object from it.
306        items: HashMap<String, String>,
307        /// Other Key-Value pairs, that will be used in a generic "where" clause
308        other: HashSet<String>,
309    }
310
311    impl Query {
312        /// Parses the query from a string
313        pub fn parse<S: AsRef<str>>(query_str: S) -> Query {
314            let mut items = HashMap::new();
315            let mut other = HashSet::new();
316            // Split items at '&'
317            for encoded in query_str.as_ref().split('&') {
318                // Decode url pattern
319                // let key_value = urlencoding::decode(encoded).unwrap_or_default();
320                let key_value = encoded; // TODO: urlencoding !
321
322                // First, we have to check if there is a '<' or '>' sign in the string,
323                // because in this case we have to handle it differently
324                if key_value.contains(['<', '>']) {
325                    // In this case we just remember the entire statement as whole
326                    other.insert(key_value.replace('+', " "));
327                } else {
328                    // Check for key-value pairs
329                    let mut iter = key_value.splitn(2, '=');
330                    let key = iter.next().unwrap_or_default();
331                    let value = iter.next().unwrap_or_default();
332                    // Ignore empty values
333                    if !value.is_empty() && !key.is_empty() {
334                        match key {
335                            // Check weather or not we have a special keyword
336                            "page" | "per_page" | "sort" | "order" | "action" => {
337                                items.insert(key.to_string(), value.to_string());
338                            }
339                            // Check for contract_id and process_id, as they require special parsing
340                            "contract_id" | "contract-id" => {
341                                if let Ok(id) = ContractId::parse_str(value) {
342                                    other.insert(format!("contract_id={}", id));
343                                }
344                            }
345                            "agent_id" | "agent-id" | "process_id" | "process-id" => {
346                                if let Ok(id) = AgentId::parse_str(value) {
347                                    other.insert(format!("agent_id={}", id));
348                                }
349                            }
350                            // Otherwise just remember the entire statement as whole
351                            _ => {
352                                other.insert(format!("{}={}", key, value.replace('+', " "),));
353                            }
354                        }
355                    }
356                }
357            }
358            Query { items, other }
359        }
360
361        /// Returns pagination element if present
362        pub fn pagination(&self) -> Option<Pagination> {
363            let page_item = self.items.get("page")?;
364            let per_page_item = self.items.get("per_page")?;
365            let page = usize::from_str(page_item).ok()?;
366            let per_page = usize::from_str(per_page_item).ok()?;
367            Some(Pagination { page, per_page })
368        }
369
370        /// Returns sorting element if present
371        pub fn sorting(&self) -> Option<Sorting> {
372            let sort_by = self.items.get("sort")?.clone();
373            let order_item = match self.items.get("order") {
374                Some(item) => item,
375                None => {
376                    return Some(Sorting {
377                        sort_by,
378                        order: Order::Ascending,
379                    })
380                }
381            };
382            let order = match order_item.to_ascii_lowercase().as_ref() {
383                "ascending" | "asc" => Order::Ascending,
384                "descending" | "desc" => Order::Descending,
385                // Everything else is invalid
386                _ => return None,
387            };
388            Some(Sorting { sort_by, order })
389        }
390
391        /// Returns action-query if present
392        pub fn action_query(&self) -> Option<String> {
393            let _action = self.items.get("action")?;
394            todo!("re-implement this for new action system")
395        }
396
397        pub fn contains_other(&self) -> bool {
398            !self.other.is_empty()
399        }
400
401        pub fn other(&self) -> impl Iterator<Item = &str> {
402            self.other.iter().map(|s| s.as_str())
403        }
404    }
405
406    #[derive(Debug, PartialEq, Eq)]
407    pub enum Order {
408        Ascending,
409        Descending,
410    }
411
412    impl AsRef<str> for Order {
413        fn as_ref(&self) -> &str {
414            match self {
415                Order::Ascending => "ASC",
416                Order::Descending => "DESC",
417            }
418        }
419    }
420
421    impl Display for Order {
422        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
423            write!(f, "{}", self.as_ref())
424        }
425    }
426
427    #[derive(Debug, PartialEq, Eq)]
428    pub struct Sorting {
429        pub sort_by: String,
430        pub order: Order,
431    }
432
433    /// Simple struct that is used to add pagination to some endpoint like: `/endpoint?page=1&per_page=10`
434    ///
435    /// The page numbers start at "1", so they match what you would display in a frontend.
436    ///
437    /// The default implementation returns you `page=1` and `per_page=1000`.
438    #[derive(Serialize, Clone)]
439    pub struct Pagination {
440        pub page: usize,
441        pub per_page: usize,
442    }
443
444    impl Default for Pagination {
445        fn default() -> Self {
446            Self {
447                page: 1,
448                per_page: 100,
449            }
450        }
451    }
452
453    impl Pagination {
454        /// Extracts a pagination from some query string (if any).
455        ///
456        /// The query can contain other elements aswell and there is no necessity for the pieces
457        /// `page` and `per_page` to be two consecutive elements in the query.
458        ///
459        /// As long as there is a `page={}` element and a `per_page={}` element,
460        /// this function will successfully parse and return the `Pagination` struct.
461        pub fn from_query(query: Option<&str>) -> Option<Pagination> {
462            let query = query?;
463            let mut page_str: Option<&str> = None;
464            let mut per_page_str: Option<&str> = None;
465            for piece in query.split('&') {
466                if piece.starts_with("page=") {
467                    page_str = Some(piece);
468                } else if piece.starts_with("per_page=") || piece.starts_with("per-page") {
469                    per_page_str = Some(piece);
470                }
471                if page_str.is_some() && per_page_str.is_some() {
472                    break;
473                }
474            }
475            // NOTE: We want the per_page or page to be set to the value that Pagionation::default() assigns.
476            // Using clippys suggestion would overwrite the value with the default for the type (which is 0).
477            #[allow(clippy::field_reassign_with_default)]
478            match (page_str, per_page_str) {
479                (Some(page_str), Some(per_page_str)) => {
480                    let page_num: &str = page_str.split('=').nth(1)?;
481                    let per_page_num: &str = per_page_str.split('=').nth(1)?;
482                    let page = usize::from_str(page_num).ok()?;
483                    let per_page = usize::from_str(per_page_num).ok()?;
484                    Some(Pagination { page, per_page })
485                }
486                (Some(page_str), None) => {
487                    let page_num: &str = page_str.split('=').nth(1)?;
488                    let page = usize::from_str(page_num).ok()?;
489                    let mut pagination = Pagination::default();
490                    pagination.page = page;
491                    Some(pagination)
492                }
493                (None, Some(per_page_str)) => {
494                    let per_page_num: &str = per_page_str.split('=').nth(1)?;
495                    let per_page = usize::from_str(per_page_num).ok()?;
496                    let mut pagination = Pagination::default();
497                    pagination.per_page = per_page;
498                    Some(pagination)
499                }
500                _ => None,
501            }
502        }
503
504        /// Converts the pagination into a range to iterate over
505        ///
506        /// Note: The index of the range starts at "0" and not at "1",
507        /// like the pagination does. No manual conversion needed.
508        pub fn to_range(&self) -> std::ops::Range<usize> {
509            self.clone().into()
510        }
511    }
512
513    impl From<Pagination> for std::ops::Range<usize> {
514        fn from(value: Pagination) -> Self {
515            let start = value.page.saturating_sub(1) * value.per_page;
516            let end = value.page * value.per_page;
517            Self { start, end }
518        }
519    }
520
521    #[cfg(test)]
522    mod query_tests {
523        use super::*;
524
525        #[test]
526        fn pagination() {
527            let queries = [
528                "page=10&per_page=2312",                     // Simple query
529                "per_page=2312&page=10", // Simple query, but fields are reversed
530                "something_else=null&page=10&per_page=2312", // Complex query that contains a pagination element
531                "page=10&something_else=null&per_page=2312", // Complex query that contains a pagination element
532            ];
533            for query in queries {
534                let pagination = Pagination::from_query(Some(query));
535                assert!(pagination.is_some());
536                let pagination = pagination.unwrap();
537                assert_eq!(pagination.page, 10);
538                assert_eq!(pagination.per_page, 2312);
539                // The Query object must produce the same result
540                let pagination = Query::parse(query).pagination();
541                assert!(pagination.is_some());
542                let pagination = pagination.unwrap();
543                assert_eq!(pagination.page, 10);
544                assert_eq!(pagination.per_page, 2312);
545            }
546            assert!(Pagination::from_query(None).is_none());
547            let bad_queries = [
548                "page=10&per_page=",    // Missing argument
549                "per_page=2312&page=",  // Missing argument, but fields are reversed
550                "page=id&perpage=2312", // Misspelling
551                "something_else=null",  // Something else
552            ];
553            for query in bad_queries {
554                let pagination = Pagination::from_query(Some(query));
555                assert!(
556                    pagination.is_none(),
557                    "This query should not work: {}",
558                    query
559                );
560                // The Query object must produce the same result
561                let pagination = Query::parse(query).pagination();
562                assert!(
563                    pagination.is_none(),
564                    "This query should not work: {}",
565                    query
566                );
567            }
568        }
569
570        #[test]
571        fn keyvalue() {
572            let good_queries = [
573                "key=id&value=2312",                     // Simple query
574                "value=2312&key=id",                     // Simple query, but fields are reversed
575                "something_else=null&key=id&value=2312", // Complex query
576                "key=id&something_else=null&value=2312", // Complex query
577            ];
578            for query in good_queries {
579                let key_value = Query::parse(query);
580                assert!(key_value.contains_other());
581                assert!(key_value.other().any(|s| s == "key=id"));
582                assert!(key_value.other().any(|s| s == "value=2312"));
583            }
584            let bad_queries = [
585                "key=&value=",         // Missing argument
586                "value=&key=",         // Missing argument, but fields are reversed
587                "ky=id&vaue=2312",     // Misspelling
588                "something_else=null", // Something else
589            ];
590            for query in bad_queries {
591                let key_value = Query::parse(query);
592                assert!(!key_value.other().any(|s| s == "key=id"));
593                assert!(!key_value.other().any(|s| s == "value=2312"));
594            }
595        }
596
597        #[test]
598        fn sorting() {
599            let good_queries_asc = [
600                "sort=sensor_id&order=asc",
601                "sort=sensor_id&order=ascending",
602                "order=ascending&sort=sensor_id",
603                "order=asc&sort=sensor_id",
604                "sort=sensor_id&order=aSc",
605                "sort=sensor_id&order=ASCending",
606                "order=Ascending&sort=sensor_id",
607                "order=Asc&sort=sensor_id",
608                "sort=sensor_id", // if we don't specify the order, it is ascending by default
609                "sort=sensor_id&order", // defaults to ascending, because order is ignored
610            ];
611            for query in good_queries_asc {
612                let sorting = Query::parse(query).sorting();
613                assert!(sorting.is_some());
614                let sorting = sorting.unwrap();
615                assert_eq!(
616                    sorting,
617                    Sorting {
618                        sort_by: "sensor_id".to_string(),
619                        order: Order::Ascending
620                    }
621                );
622            }
623            let good_queries_desc = [
624                "sort=sensor_id&order=desc",
625                "sort=sensor_id&order=descending",
626                "order=descending&sort=sensor_id",
627                "order=desc&sort=sensor_id",
628                "sort=sensor_id&order=dESc",
629                "sort=sensor_id&order=Descending",
630                "order=Descending&sort=sensor_id",
631                "order=Desc&sort=sensor_id",
632            ];
633            for query in good_queries_desc {
634                let sorting = Query::parse(query).sorting();
635                assert!(sorting.is_some());
636                let sorting = sorting.unwrap();
637                assert_eq!(
638                    sorting,
639                    Sorting {
640                        sort_by: "sensor_id".to_string(),
641                        order: Order::Descending
642                    }
643                );
644            }
645            let bad_queries = [
646                "sort=sensor_id&order=something", // Wrong order
647                "sort=&order=descending",         // Missing key
648                "order=ascending",                // Missing sort
649            ];
650            for query in bad_queries {
651                let sorting = Query::parse(query).sorting();
652                assert!(sorting.is_none(), "This query should not work: {}", query);
653            }
654        }
655
656        #[test]
657        fn parse_contract_id() {
658            let contract_ids = [
659                "contract-id=c073e869-4ae1-892c-aba7-2ad8318d5c12",
660                "contract_id=c073e869-4ae1-892c-aba7-2ad8318d5c12",
661            ];
662            for id in contract_ids {
663                let query = Query::parse(id);
664                assert!(query.contains_other(), "Query should contain contract-id");
665                let cid = query.other().next().unwrap();
666                assert_eq!(cid, "contract_id=c073e869-4ae1-892c-aba7-2ad8318d5c12");
667            }
668            let agent_ids = [
669                "process_id=a073e869-4ae1-892c-aba7-2ad8318d5c12",
670                "process-id=a073e869-4ae1-892c-aba7-2ad8318d5c12",
671                "agent-id=a073e869-4ae1-892c-aba7-2ad8318d5c12",
672                "agent_id=a073e869-4ae1-892c-aba7-2ad8318d5c12",
673            ];
674            for id in agent_ids {
675                let query = Query::parse(id);
676                assert!(query.contains_other(), "Query should contain contract-id");
677                let cid = query.other().next().unwrap();
678                assert_eq!(cid, "agent_id=a073e869-4ae1-892c-aba7-2ad8318d5c12");
679            }
680        }
681    }
682}
683
684#[cfg(test)]
685mod tests {
686    use super::*;
687
688    #[test]
689    fn test_valid_response() {
690        let head = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nX-Test: 123\r\n\r\n";
691        let body = b"Hello, world!".to_vec();
692
693        let response = build_response_from_parts(head, body.clone()).unwrap();
694
695        assert_eq!(response.status(), StatusCode::OK);
696        assert_eq!(response.version(), Version::HTTP_11);
697        assert_eq!(
698            response.headers().get("Content-Type").unwrap(),
699            "text/plain"
700        );
701        assert_eq!(response.headers().get("X-Test").unwrap(), "123");
702        assert_eq!(response.body(), &body);
703    }
704
705    #[test]
706    fn test_valid_http_2_response() {
707        let head = "HTTP/2 204 No Content\r\nX-Empty: yes\r\n\r\n";
708        let body = Vec::new();
709
710        let response = build_response_from_parts(head, body.clone()).unwrap();
711
712        assert_eq!(response.status(), StatusCode::NO_CONTENT);
713        assert_eq!(response.version(), Version::HTTP_2);
714        assert_eq!(response.headers().get("X-Empty").unwrap(), "yes");
715        assert_eq!(response.body(), &body);
716    }
717
718    #[test]
719    fn test_missing_status_line_parts() {
720        let head = "HTTP/1.1\r\nContent-Type: text/plain\r\n\r\n";
721        let body = b"Oops".to_vec();
722
723        let result = build_response_from_parts(head, body);
724        assert!(result.is_err());
725    }
726
727    #[test]
728    fn test_invalid_http_version() {
729        let head = "HTTP/3.0 200 OK\r\nContent-Type: text/plain\r\n\r\n";
730        let body = b"Invalid".to_vec();
731
732        let result = build_response_from_parts(head, body);
733        assert!(result.is_err());
734    }
735
736    #[test]
737    fn test_invalid_status_code() {
738        let head = "HTTP/1.1 abc OK\r\nContent-Type: text/plain\r\n\r\n";
739        let body = b"Invalid".to_vec();
740
741        let result = build_response_from_parts(head, body);
742        assert!(result.is_err());
743    }
744
745    #[test]
746    fn test_malformed_header() {
747        let head = "HTTP/1.1 200 OK\r\nBad-Header-Without-Colon\r\n\r\n";
748        let body = b"BadHeader".to_vec();
749
750        let result = build_response_from_parts(head, body);
751        assert!(result.is_err());
752    }
753
754    #[test]
755    fn test_empty_head() {
756        let head = "";
757        let body = b"Empty".to_vec();
758
759        let result = build_response_from_parts(head, body);
760        assert!(result.is_err());
761    }
762}