1use bytes::Bytes;
7use http::{HeaderMap, HeaderValue, Method, Request, Response};
8use http_body_util::{BodyExt, Full};
9use serde::Serialize;
10use serde_json::Value;
11use std::collections::HashMap;
12use std::sync::Arc;
13use std::time::Duration;
14use thiserror::Error;
15use tokio::sync::RwLock;
16
17use reinhardt_di::InjectionContext;
18use reinhardt_http::{Handler as HttpHandler, Request as HttpRequest, Response as HttpResponse};
19
20use crate::response::TestResponse;
21
22#[derive(Debug, Clone, Copy, Default)]
24pub enum HttpVersion {
25 Http1Only,
27 Http2PriorKnowledge,
29 #[default]
31 Auto,
32}
33
34#[derive(Debug, Error)]
36pub enum ClientError {
37 #[error("HTTP error: {0}")]
39 Http(#[from] http::Error),
40
41 #[error("Hyper error: {0}")]
43 Hyper(#[from] hyper::Error),
44
45 #[error("Serialization error: {0}")]
47 Serialization(#[from] serde_json::Error),
48
49 #[error("Invalid header value: {0}")]
51 InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
52
53 #[error("Reqwest error: {0}")]
55 Reqwest(#[from] reqwest::Error),
56
57 #[error("Request failed: {0}")]
59 RequestFailed(String),
60}
61
62impl ClientError {
63 pub fn is_timeout(&self) -> bool {
65 match self {
66 ClientError::Reqwest(e) => e.is_timeout(),
67 _ => false,
68 }
69 }
70
71 pub fn is_connect(&self) -> bool {
73 match self {
74 ClientError::Reqwest(e) => e.is_connect(),
75 _ => false,
76 }
77 }
78
79 pub fn is_request(&self) -> bool {
81 match self {
82 ClientError::Reqwest(e) => e.is_request(),
83 ClientError::Http(_) => true,
84 ClientError::InvalidHeaderValue(_) => true,
85 ClientError::Serialization(_) => true,
86 ClientError::RequestFailed(_) => true,
87 _ => false,
88 }
89 }
90}
91
92pub type ClientResult<T> = Result<T, ClientError>;
94
95pub type RequestHandler = Arc<dyn Fn(Request<Full<Bytes>>) -> Response<Full<Bytes>> + Send + Sync>;
97
98pub struct APIClientBuilder {
113 base_url: String,
114 timeout: Option<Duration>,
115 http_version: HttpVersion,
116 cookie_store: bool,
117 framework_handler: Option<Arc<dyn HttpHandler>>,
118 di_context: Option<Arc<InjectionContext>>,
119}
120
121impl APIClientBuilder {
122 pub fn new() -> Self {
124 Self {
125 base_url: "http://testserver".to_string(),
126 timeout: None,
127 http_version: HttpVersion::Auto,
128 cookie_store: false,
129 framework_handler: None,
130 di_context: None,
131 }
132 }
133
134 pub fn base_url(mut self, url: impl Into<String>) -> Self {
136 self.base_url = url.into();
137 self
138 }
139
140 pub fn timeout(mut self, duration: Duration) -> Self {
142 self.timeout = Some(duration);
143 self
144 }
145
146 pub fn http_version(mut self, version: HttpVersion) -> Self {
148 self.http_version = version;
149 self
150 }
151
152 pub fn http1_only(mut self) -> Self {
154 self.http_version = HttpVersion::Http1Only;
155 self
156 }
157
158 pub fn http2_prior_knowledge(mut self) -> Self {
160 self.http_version = HttpVersion::Http2PriorKnowledge;
161 self
162 }
163
164 pub fn cookie_store(mut self, enabled: bool) -> Self {
166 self.cookie_store = enabled;
167 self
168 }
169
170 pub fn handler(mut self, handler: impl HttpHandler + 'static) -> Self {
177 self.framework_handler = Some(Arc::new(handler));
178 self
179 }
180
181 pub fn di_context(mut self, ctx: Arc<InjectionContext>) -> Self {
186 self.di_context = Some(ctx);
187 self
188 }
189
190 pub fn build(self) -> APIClient {
192 let mut client_builder = reqwest::Client::builder();
193
194 if let Some(timeout) = self.timeout {
196 client_builder = client_builder.timeout(timeout);
197 }
198
199 match self.http_version {
201 HttpVersion::Http1Only => {
202 client_builder = client_builder.http1_only();
203 }
204 HttpVersion::Http2PriorKnowledge => {
205 client_builder = client_builder.http2_prior_knowledge();
206 }
207 HttpVersion::Auto => {
208 }
210 }
211
212 if self.cookie_store {
214 client_builder = client_builder.cookie_store(true);
215 }
216
217 let http_client = client_builder
218 .build()
219 .expect("Failed to build reqwest client");
220
221 let mut client = APIClient {
222 base_url: self.base_url,
223 default_headers: Arc::new(RwLock::new(HeaderMap::new())),
224 cookies: Arc::new(RwLock::new(HashMap::new())),
225 user: Arc::new(RwLock::new(None)),
226 handler: None,
227 async_handler: None,
228 handler_di_context: None,
229 http_client,
230 use_cookie_store: self.cookie_store,
231 };
232
233 if let Some(fw_handler) = self.framework_handler {
235 client.async_handler = Some(fw_handler);
236 client.handler_di_context = self.di_context;
237
238 if let Ok(mut headers) = client.default_headers.try_write()
240 && let Ok(origin) = HeaderValue::from_str(&client.base_url)
241 {
242 headers.insert(http::header::ORIGIN, origin);
243 }
244 }
245
246 client
247 }
248}
249
250impl Default for APIClientBuilder {
251 fn default() -> Self {
252 Self::new()
253 }
254}
255
256pub struct APIClient {
275 base_url: String,
277
278 default_headers: Arc<RwLock<HeaderMap>>,
280
281 cookies: Arc<RwLock<HashMap<String, String>>>,
283
284 user: Arc<RwLock<Option<Value>>>,
286
287 handler: Option<RequestHandler>,
289
290 async_handler: Option<Arc<dyn HttpHandler>>,
292
293 handler_di_context: Option<Arc<InjectionContext>>,
295
296 http_client: reqwest::Client,
298
299 use_cookie_store: bool,
301}
302
303impl APIClient {
304 pub fn new() -> Self {
315 APIClientBuilder::new().build()
316 }
317
318 pub fn with_base_url(base_url: impl Into<String>) -> Self {
329 APIClientBuilder::new().base_url(base_url).build()
330 }
331
332 pub fn from_handler(handler: impl HttpHandler + 'static) -> Self {
353 APIClientBuilder::new().handler(handler).build()
354 }
355
356 pub fn builder() -> APIClientBuilder {
370 APIClientBuilder::new()
371 }
372 pub fn base_url(&self) -> &str {
374 &self.base_url
375 }
376 pub fn set_handler<F>(&mut self, handler: F)
395 where
396 F: Fn(Request<Full<Bytes>>) -> Response<Full<Bytes>> + Send + Sync + 'static,
397 {
398 self.handler = Some(Arc::new(handler));
399 }
400 pub async fn set_header(
413 &self,
414 name: impl AsRef<str>,
415 value: impl AsRef<str>,
416 ) -> ClientResult<()> {
417 let mut headers = self.default_headers.write().await;
418 let header_name: http::header::HeaderName = name.as_ref().parse().map_err(|_| {
419 ClientError::RequestFailed(format!("Invalid header name: {}", name.as_ref()))
420 })?;
421 headers.insert(header_name, HeaderValue::from_str(value.as_ref())?);
422 Ok(())
423 }
424 pub async fn credentials(&self, username: &str, password: &str) -> ClientResult<()> {
437 let encoded = base64::encode(format!("{}:{}", username, password));
438 self.set_header("Authorization", format!("Basic {}", encoded))
439 .await
440 }
441 pub async fn clear_auth(&self) -> ClientResult<()> {
454 {
455 let mut current_user = self.user.write().await;
456 *current_user = None;
457 }
458 let mut cookies = self.cookies.write().await;
459 cookies.clear();
460 drop(cookies);
461 let mut headers = self.default_headers.write().await;
463 headers.remove("authorization");
464 headers.remove("x-mfa-code");
465 headers.remove("x-test-user");
466 Ok(())
467 }
468
469 pub async fn set_cookie(&self, name: &str, value: &str) -> ClientResult<()> {
475 validate_cookie_key(name);
476 validate_cookie_value(value);
477 let mut cookies = self.cookies.write().await;
478 cookies.insert(name.to_string(), value.to_string());
479 Ok(())
480 }
481
482 pub async fn remove_cookie(&self, name: &str) -> ClientResult<()> {
484 let mut cookies = self.cookies.write().await;
485 cookies.remove(name);
486 Ok(())
487 }
488
489 pub async fn logout(&self) -> ClientResult<()> {
493 self.clear_auth().await
494 }
495
496 #[cfg(native)]
507 pub fn auth(&self) -> crate::auth::AuthBuilder<'_> {
508 crate::auth::AuthBuilder::new(self)
509 }
510
511 pub async fn cleanup(&self) {
534 {
536 let mut current_user = self.user.write().await;
537 *current_user = None;
538 }
539
540 {
542 let mut cookies = self.cookies.write().await;
543 cookies.clear();
544 }
545
546 {
548 let mut headers = self.default_headers.write().await;
549 headers.clear();
550 }
551 }
552 pub async fn get(&self, path: &str) -> ClientResult<TestResponse> {
566 self.request(Method::GET, path, None, None).await
567 }
568 pub async fn post<T: Serialize>(
584 &self,
585 path: &str,
586 data: &T,
587 format: &str,
588 ) -> ClientResult<TestResponse> {
589 let body = self.serialize_data(data, format)?;
590 let content_type = self.get_content_type(format);
591 self.request(Method::POST, path, Some(body), Some(content_type))
592 .await
593 }
594 pub async fn put<T: Serialize>(
610 &self,
611 path: &str,
612 data: &T,
613 format: &str,
614 ) -> ClientResult<TestResponse> {
615 let body = self.serialize_data(data, format)?;
616 let content_type = self.get_content_type(format);
617 self.request(Method::PUT, path, Some(body), Some(content_type))
618 .await
619 }
620 pub async fn patch<T: Serialize>(
636 &self,
637 path: &str,
638 data: &T,
639 format: &str,
640 ) -> ClientResult<TestResponse> {
641 let body = self.serialize_data(data, format)?;
642 let content_type = self.get_content_type(format);
643 self.request(Method::PATCH, path, Some(body), Some(content_type))
644 .await
645 }
646 pub async fn delete(&self, path: &str) -> ClientResult<TestResponse> {
660 self.request(Method::DELETE, path, None, None).await
661 }
662 pub async fn head(&self, path: &str) -> ClientResult<TestResponse> {
676 self.request(Method::HEAD, path, None, None).await
677 }
678 pub async fn options(&self, path: &str) -> ClientResult<TestResponse> {
692 self.request(Method::OPTIONS, path, None, None).await
693 }
694
695 pub async fn get_with_headers(
708 &self,
709 path: &str,
710 headers: &[(&str, &str)],
711 ) -> ClientResult<TestResponse> {
712 self.request_with_extra_headers(Method::GET, path, None, None, headers)
713 .await
714 }
715
716 pub async fn post_raw_with_headers(
736 &self,
737 path: &str,
738 body: &[u8],
739 content_type: &str,
740 headers: &[(&str, &str)],
741 ) -> ClientResult<TestResponse> {
742 self.request_with_extra_headers(
743 Method::POST,
744 path,
745 Some(Bytes::copy_from_slice(body)),
746 Some(content_type),
747 headers,
748 )
749 .await
750 }
751
752 pub async fn post_raw(
767 &self,
768 path: &str,
769 body: &[u8],
770 content_type: &str,
771 ) -> ClientResult<TestResponse> {
772 self.request(
773 Method::POST,
774 path,
775 Some(Bytes::copy_from_slice(body)),
776 Some(content_type),
777 )
778 .await
779 }
780
781 async fn request(
783 &self,
784 method: Method,
785 path: &str,
786 body: Option<Bytes>,
787 content_type: Option<&str>,
788 ) -> ClientResult<TestResponse> {
789 self.request_with_extra_headers(method, path, body, content_type, &[])
790 .await
791 }
792
793 async fn request_with_extra_headers(
798 &self,
799 method: Method,
800 path: &str,
801 body: Option<Bytes>,
802 content_type: Option<&str>,
803 extra_headers: &[(&str, &str)],
804 ) -> ClientResult<TestResponse> {
805 let url = if path.starts_with("http://") || path.starts_with("https://") {
806 path.to_string()
807 } else {
808 format!("{}{}", self.base_url, path)
809 };
810
811 let mut req_builder = Request::builder().method(method).uri(url);
812
813 let default_headers = self.default_headers.read().await;
815 for (name, value) in default_headers.iter() {
816 req_builder = req_builder.header(name, value);
817 }
818
819 for (name, value) in extra_headers {
821 req_builder = req_builder.header(*name, *value);
822 }
823
824 if let Some(ct) = content_type {
826 req_builder = req_builder.header("Content-Type", ct);
827 }
828
829 let cookies = self.cookies.read().await;
831 if !cookies.is_empty() {
832 let cookie_header = cookies
833 .iter()
834 .map(|(k, v)| {
835 validate_cookie_key(k);
836 validate_cookie_value(v);
837 format!("{}={}", k, v)
838 })
839 .collect::<Vec<_>>()
840 .join("; ");
841 req_builder = req_builder.header("Cookie", cookie_header);
842 }
843
844 let user = self.user.read().await;
846 if user.is_some() {
847 req_builder = req_builder.header("X-Test-User", "authenticated");
849 }
850
851 let request = if let Some(body_bytes) = body {
853 req_builder.body(Full::new(body_bytes))?
854 } else {
855 req_builder.body(Full::new(Bytes::new()))?
856 };
857
858 let response = if let Some(async_handler) = &self.async_handler {
860 let (parts, body) = request.into_parts();
862 let body_bytes = body
863 .collect()
864 .await
865 .map(|c| c.to_bytes())
866 .unwrap_or_else(|_| Bytes::new());
867
868 let mut fw_request = HttpRequest::builder()
869 .method(parts.method)
870 .uri(parts.uri)
871 .version(parts.version)
872 .headers(parts.headers)
873 .body(body_bytes)
874 .build()
875 .expect("Failed to build reinhardt request");
876
877 if let Some(ctx) = &self.handler_di_context {
878 fw_request.set_di_context(Arc::clone(ctx));
879 }
880
881 let fw_response = async_handler
882 .handle(fw_request)
883 .await
884 .unwrap_or_else(HttpResponse::from);
885
886 let mut builder = http::Response::builder().status(fw_response.status);
887 for (key, value) in fw_response.headers.iter() {
888 builder = builder.header(key, value);
889 }
890 builder
891 .body(Full::new(fw_response.body))
892 .expect("Failed to build http::Response")
893 } else if let Some(handler) = &self.handler {
894 handler(request)
896 } else {
897 let (parts, body) = request.into_parts();
899
900 let url = if parts.uri.scheme_str().is_some() {
902 parts.uri.to_string()
904 } else {
905 format!(
907 "{}{}",
908 self.base_url.trim_end_matches('/'),
909 parts.uri.path()
910 )
911 };
912
913 let mut reqwest_request = self.http_client.request(
915 reqwest::Method::from_bytes(parts.method.as_str().as_bytes()).unwrap(),
916 &url,
917 );
918
919 for (name, value) in parts.headers.iter() {
921 if self.use_cookie_store && name.as_str().eq_ignore_ascii_case("cookie") {
922 continue;
923 }
924 reqwest_request = reqwest_request.header(name.as_str(), value.as_bytes());
925 }
926
927 let body_bytes = body
929 .collect()
930 .await
931 .map(|c| c.to_bytes())
932 .unwrap_or_else(|_| Bytes::new());
933 if !body_bytes.is_empty() {
934 reqwest_request = reqwest_request.body(body_bytes.to_vec());
935 }
936
937 let reqwest_response = reqwest_request.send().await?;
939
940 let status = reqwest_response.status();
942 let version = reqwest_response.version();
943 let headers = reqwest_response.headers().clone();
944 let body_bytes = reqwest_response.bytes().await?;
945
946 let mut response_builder = Response::builder().status(status).version(version);
947 for (name, value) in headers.iter() {
948 response_builder = response_builder.header(name, value);
949 }
950
951 response_builder.body(Full::new(body_bytes))?
952 };
953
954 let (parts, response_body) = response.into_parts();
956 let body_data = response_body
957 .collect()
958 .await
959 .map(|collected| collected.to_bytes())
960 .unwrap_or_else(|_| Bytes::new());
961
962 Ok(TestResponse::with_body_and_version(
963 parts.status,
964 parts.headers,
965 body_data,
966 parts.version,
967 ))
968 }
969
970 fn serialize_data<T: Serialize>(&self, data: &T, format: &str) -> ClientResult<Bytes> {
972 match format {
973 "json" => {
974 let json = serde_json::to_vec(data)?;
975 Ok(Bytes::from(json))
976 }
977 "form" => {
978 let json_value = serde_json::to_value(data)?;
980 if let Value::Object(map) = json_value {
981 let form_data = map
982 .iter()
983 .map(|(k, v)| {
984 let value_str = match v {
985 Value::String(s) => s.clone(),
986 _ => v.to_string(),
987 };
988 format!(
989 "{}={}",
990 urlencoding::encode(k),
991 urlencoding::encode(&value_str)
992 )
993 })
994 .collect::<Vec<_>>()
995 .join("&");
996 Ok(Bytes::from(form_data))
997 } else {
998 Err(ClientError::RequestFailed(
999 "Expected object for form data".to_string(),
1000 ))
1001 }
1002 }
1003 _ => Err(ClientError::RequestFailed(format!(
1004 "Unsupported format: {}",
1005 format
1006 ))),
1007 }
1008 }
1009
1010 fn get_content_type(&self, format: &str) -> &str {
1012 match format {
1013 "json" => "application/json",
1014 "form" => "application/x-www-form-urlencoded",
1015 _ => "application/octet-stream",
1016 }
1017 }
1018}
1019
1020fn validate_cookie_key(key: &str) {
1028 assert!(!key.is_empty(), "cookie key must not be empty");
1029 assert!(
1030 !key.contains('='),
1031 "cookie key must not contain '=' (found in key: {:?})",
1032 key
1033 );
1034 assert!(
1035 !key.contains(';'),
1036 "cookie key must not contain ';' (found in key: {:?})",
1037 key
1038 );
1039 assert!(
1040 !key.chars().any(|c| c.is_ascii_whitespace()),
1041 "cookie key must not contain whitespace (found in key: {:?})",
1042 key
1043 );
1044 assert!(
1045 !key.chars().any(|c| c.is_control()),
1046 "cookie key must not contain control characters (found in key: {:?})",
1047 key
1048 );
1049}
1050
1051fn validate_cookie_value(value: &str) {
1059 assert!(!value.contains(';'), "cookie value must not contain ';'");
1060 assert!(
1061 !value.contains('\r') && !value.contains('\n'),
1062 "cookie value must not contain newlines"
1063 );
1064 assert!(
1065 !value.chars().any(|c| c.is_control()),
1066 "cookie value must not contain control characters"
1067 );
1068}
1069
1070impl Default for APIClient {
1071 fn default() -> Self {
1072 Self::new()
1073 }
1074}
1075
1076mod base64 {
1078 pub(super) fn encode(input: String) -> String {
1079 use base64_simd::STANDARD;
1081 STANDARD.encode_to_string(input.as_bytes())
1082 }
1083}
1084
1085mod urlencoding {
1087 pub(super) fn encode(input: &str) -> String {
1088 url::form_urlencoded::byte_serialize(input.as_bytes()).collect()
1089 }
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094 use super::*;
1095 use async_trait::async_trait;
1096 use reinhardt_core::exception::{Error as HttpError, Result as HttpResult};
1097 use rstest::rstest;
1098
1099 struct EchoHandler;
1102
1103 #[async_trait]
1104 impl HttpHandler for EchoHandler {
1105 async fn handle(&self, request: HttpRequest) -> HttpResult<HttpResponse> {
1106 let path = request.uri.path().to_string();
1107 let has_custom = request.headers.get("X-Custom").is_some();
1108 let content_type = request
1109 .headers
1110 .get("Content-Type")
1111 .and_then(|v| v.to_str().ok())
1112 .unwrap_or("")
1113 .to_string();
1114
1115 let mut response = HttpResponse::ok().with_body(path.clone());
1116 response = response.try_with_header("X-Echo-Path", &path)?;
1117
1118 if has_custom {
1119 response = response.try_with_header("X-Echo-Custom", "present")?;
1120 }
1121 if !content_type.is_empty() {
1122 response = response.try_with_header("X-Echo-Content-Type", &content_type)?;
1123 }
1124 Ok(response)
1125 }
1126 }
1127
1128 struct ErrorHandler;
1130
1131 #[async_trait]
1132 impl HttpHandler for ErrorHandler {
1133 async fn handle(&self, _request: HttpRequest) -> HttpResult<HttpResponse> {
1134 Err(HttpError::NotFound("test resource".to_string()))
1135 }
1136 }
1137
1138 #[rstest]
1139 #[tokio::test]
1140 async fn test_from_handler_basic() {
1141 let client = APIClient::from_handler(EchoHandler);
1143
1144 let response = client.get("/test/path/").await.expect("request failed");
1146
1147 assert_eq!(response.status(), http::StatusCode::OK);
1149 assert_eq!(response.body().as_ref(), b"/test/path/");
1150 }
1151
1152 #[rstest]
1153 #[tokio::test]
1154 async fn test_from_handler_post_body() {
1155 let client = APIClient::from_handler(EchoHandler);
1157 let body = serde_json::json!({"key": "value"});
1158
1159 let response = client
1161 .post("/echo/", &body, "json")
1162 .await
1163 .expect("request failed");
1164
1165 assert_eq!(response.status(), http::StatusCode::OK);
1167 assert_eq!(
1168 response
1169 .header("X-Echo-Content-Type")
1170 .expect("missing header"),
1171 "application/json"
1172 );
1173 }
1174
1175 #[rstest]
1176 #[tokio::test]
1177 async fn test_from_handler_headers() {
1178 let client = APIClient::from_handler(EchoHandler);
1180 client
1181 .set_header("X-Custom", "test-value")
1182 .await
1183 .expect("set_header failed");
1184
1185 let response = client.get("/test/").await.expect("request failed");
1187
1188 assert_eq!(response.status(), http::StatusCode::OK);
1190 assert_eq!(
1191 response.header("X-Echo-Custom").expect("missing header"),
1192 "present"
1193 );
1194 }
1195
1196 #[rstest]
1197 #[tokio::test]
1198 async fn test_from_handler_error_conversion() {
1199 let client = APIClient::from_handler(ErrorHandler);
1201
1202 let response = client.get("/anything/").await.expect("request failed");
1204
1205 assert_eq!(response.status(), http::StatusCode::NOT_FOUND);
1207 }
1208
1209 #[rstest]
1210 #[tokio::test]
1211 async fn test_from_handler_origin_header() {
1212 let client = APIClient::from_handler(EchoHandler);
1214
1215 let headers = client.default_headers.read().await;
1217
1218 let origin = headers
1220 .get(http::header::ORIGIN)
1221 .expect("Origin header not set");
1222 assert_eq!(origin.to_str().unwrap(), "http://testserver");
1223 }
1224
1225 #[rstest]
1226 #[tokio::test]
1227 async fn test_builder_with_handler() {
1228 let client = APIClient::builder()
1230 .base_url("http://mytest")
1231 .handler(EchoHandler)
1232 .build();
1233
1234 let response = client.get("/api/").await.expect("request failed");
1236
1237 assert_eq!(response.status(), http::StatusCode::OK);
1239 let headers = client.default_headers.read().await;
1240 let origin = headers
1241 .get(http::header::ORIGIN)
1242 .expect("Origin header not set");
1243 assert_eq!(origin.to_str().unwrap(), "http://mytest");
1244 }
1245
1246 #[rstest]
1247 fn test_validate_cookie_key_accepts_valid_key() {
1248 let key = "session_id";
1250
1251 validate_cookie_key(key);
1253 }
1254
1255 #[rstest]
1256 #[should_panic(expected = "must not be empty")]
1257 fn test_validate_cookie_key_rejects_empty() {
1258 let key = "";
1260
1261 validate_cookie_key(key);
1263 }
1264
1265 #[rstest]
1266 #[should_panic(expected = "must not contain '='")]
1267 fn test_validate_cookie_key_rejects_equals_sign() {
1268 let key = "key=value";
1270
1271 validate_cookie_key(key);
1273 }
1274
1275 #[rstest]
1276 #[should_panic(expected = "must not contain ';'")]
1277 fn test_validate_cookie_key_rejects_semicolon() {
1278 let key = "key;injection";
1280
1281 validate_cookie_key(key);
1283 }
1284
1285 #[rstest]
1286 #[should_panic(expected = "must not contain whitespace")]
1287 fn test_validate_cookie_key_rejects_whitespace() {
1288 let key = "key name";
1290
1291 validate_cookie_key(key);
1293 }
1294
1295 #[rstest]
1296 #[should_panic(expected = "must not contain control characters")]
1297 fn test_validate_cookie_key_rejects_control_chars() {
1298 let key = "key\x00name";
1300
1301 validate_cookie_key(key);
1303 }
1304
1305 #[rstest]
1306 fn test_validate_cookie_value_accepts_valid_value() {
1307 let value = "abc123-token";
1309
1310 validate_cookie_value(value);
1312 }
1313
1314 #[rstest]
1315 fn test_validate_cookie_value_accepts_empty() {
1316 let value = "";
1318
1319 validate_cookie_value(value);
1321 }
1322
1323 #[rstest]
1324 #[should_panic(expected = "must not contain ';'")]
1325 fn test_validate_cookie_value_rejects_semicolon() {
1326 let value = "value; extra=injected";
1328
1329 validate_cookie_value(value);
1331 }
1332
1333 #[rstest]
1334 #[should_panic(expected = "must not contain newlines")]
1335 fn test_validate_cookie_value_rejects_newline() {
1336 let value = "value\r\nInjected-Header: malicious";
1338
1339 validate_cookie_value(value);
1341 }
1342
1343 #[rstest]
1344 #[should_panic(expected = "must not contain control characters")]
1345 fn test_validate_cookie_value_rejects_control_chars() {
1346 let value = "value\x01hidden";
1348
1349 validate_cookie_value(value);
1351 }
1352
1353 #[rstest]
1354 #[should_panic(expected = "must not contain newlines")]
1355 fn test_validate_cookie_value_rejects_lf_only() {
1356 let value = "value\nInjected-Header: evil";
1358
1359 validate_cookie_value(value);
1361 }
1362}