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 #[deprecated(
439 since = "0.1.0-rc.16",
440 note = "use `client.auth().session()` or `client.auth().jwt()` instead"
441 )]
442 pub async fn force_authenticate(&self, user: Option<Value>) {
443 let mut current_user = self.user.write().await;
444 *current_user = user;
445 }
446 pub async fn credentials(&self, username: &str, password: &str) -> ClientResult<()> {
459 let encoded = base64::encode(format!("{}:{}", username, password));
460 self.set_header("Authorization", format!("Basic {}", encoded))
461 .await
462 }
463 pub async fn clear_auth(&self) -> ClientResult<()> {
476 #[allow(deprecated)]
477 self.force_authenticate(None).await;
478 let mut cookies = self.cookies.write().await;
479 cookies.clear();
480 drop(cookies);
481 let mut headers = self.default_headers.write().await;
483 headers.remove("authorization");
484 headers.remove("x-mfa-code");
485 headers.remove("x-test-user");
486 Ok(())
487 }
488
489 pub async fn set_cookie(&self, name: &str, value: &str) -> ClientResult<()> {
495 validate_cookie_key(name);
496 validate_cookie_value(value);
497 let mut cookies = self.cookies.write().await;
498 cookies.insert(name.to_string(), value.to_string());
499 Ok(())
500 }
501
502 pub async fn remove_cookie(&self, name: &str) -> ClientResult<()> {
504 let mut cookies = self.cookies.write().await;
505 cookies.remove(name);
506 Ok(())
507 }
508
509 pub async fn logout(&self) -> ClientResult<()> {
513 self.clear_auth().await
514 }
515
516 #[cfg(native)]
527 pub fn auth(&self) -> crate::auth::AuthBuilder<'_> {
528 crate::auth::AuthBuilder::new(self)
529 }
530
531 pub async fn cleanup(&self) {
554 #[allow(deprecated)]
556 self.force_authenticate(None).await;
557
558 {
560 let mut cookies = self.cookies.write().await;
561 cookies.clear();
562 }
563
564 {
566 let mut headers = self.default_headers.write().await;
567 headers.clear();
568 }
569 }
570 pub async fn get(&self, path: &str) -> ClientResult<TestResponse> {
584 self.request(Method::GET, path, None, None).await
585 }
586 pub async fn post<T: Serialize>(
602 &self,
603 path: &str,
604 data: &T,
605 format: &str,
606 ) -> ClientResult<TestResponse> {
607 let body = self.serialize_data(data, format)?;
608 let content_type = self.get_content_type(format);
609 self.request(Method::POST, path, Some(body), Some(content_type))
610 .await
611 }
612 pub async fn put<T: Serialize>(
628 &self,
629 path: &str,
630 data: &T,
631 format: &str,
632 ) -> ClientResult<TestResponse> {
633 let body = self.serialize_data(data, format)?;
634 let content_type = self.get_content_type(format);
635 self.request(Method::PUT, path, Some(body), Some(content_type))
636 .await
637 }
638 pub async fn patch<T: Serialize>(
654 &self,
655 path: &str,
656 data: &T,
657 format: &str,
658 ) -> ClientResult<TestResponse> {
659 let body = self.serialize_data(data, format)?;
660 let content_type = self.get_content_type(format);
661 self.request(Method::PATCH, path, Some(body), Some(content_type))
662 .await
663 }
664 pub async fn delete(&self, path: &str) -> ClientResult<TestResponse> {
678 self.request(Method::DELETE, path, None, None).await
679 }
680 pub async fn head(&self, path: &str) -> ClientResult<TestResponse> {
694 self.request(Method::HEAD, path, None, None).await
695 }
696 pub async fn options(&self, path: &str) -> ClientResult<TestResponse> {
710 self.request(Method::OPTIONS, path, None, None).await
711 }
712
713 pub async fn get_with_headers(
726 &self,
727 path: &str,
728 headers: &[(&str, &str)],
729 ) -> ClientResult<TestResponse> {
730 self.request_with_extra_headers(Method::GET, path, None, None, headers)
731 .await
732 }
733
734 pub async fn post_raw_with_headers(
754 &self,
755 path: &str,
756 body: &[u8],
757 content_type: &str,
758 headers: &[(&str, &str)],
759 ) -> ClientResult<TestResponse> {
760 self.request_with_extra_headers(
761 Method::POST,
762 path,
763 Some(Bytes::copy_from_slice(body)),
764 Some(content_type),
765 headers,
766 )
767 .await
768 }
769
770 pub async fn post_raw(
785 &self,
786 path: &str,
787 body: &[u8],
788 content_type: &str,
789 ) -> ClientResult<TestResponse> {
790 self.request(
791 Method::POST,
792 path,
793 Some(Bytes::copy_from_slice(body)),
794 Some(content_type),
795 )
796 .await
797 }
798
799 async fn request(
801 &self,
802 method: Method,
803 path: &str,
804 body: Option<Bytes>,
805 content_type: Option<&str>,
806 ) -> ClientResult<TestResponse> {
807 self.request_with_extra_headers(method, path, body, content_type, &[])
808 .await
809 }
810
811 async fn request_with_extra_headers(
816 &self,
817 method: Method,
818 path: &str,
819 body: Option<Bytes>,
820 content_type: Option<&str>,
821 extra_headers: &[(&str, &str)],
822 ) -> ClientResult<TestResponse> {
823 let url = if path.starts_with("http://") || path.starts_with("https://") {
824 path.to_string()
825 } else {
826 format!("{}{}", self.base_url, path)
827 };
828
829 let mut req_builder = Request::builder().method(method).uri(url);
830
831 let default_headers = self.default_headers.read().await;
833 for (name, value) in default_headers.iter() {
834 req_builder = req_builder.header(name, value);
835 }
836
837 for (name, value) in extra_headers {
839 req_builder = req_builder.header(*name, *value);
840 }
841
842 if let Some(ct) = content_type {
844 req_builder = req_builder.header("Content-Type", ct);
845 }
846
847 let cookies = self.cookies.read().await;
849 if !cookies.is_empty() {
850 let cookie_header = cookies
851 .iter()
852 .map(|(k, v)| {
853 validate_cookie_key(k);
854 validate_cookie_value(v);
855 format!("{}={}", k, v)
856 })
857 .collect::<Vec<_>>()
858 .join("; ");
859 req_builder = req_builder.header("Cookie", cookie_header);
860 }
861
862 let user = self.user.read().await;
864 if user.is_some() {
865 req_builder = req_builder.header("X-Test-User", "authenticated");
867 }
868
869 let request = if let Some(body_bytes) = body {
871 req_builder.body(Full::new(body_bytes))?
872 } else {
873 req_builder.body(Full::new(Bytes::new()))?
874 };
875
876 let response = if let Some(async_handler) = &self.async_handler {
878 let (parts, body) = request.into_parts();
880 let body_bytes = body
881 .collect()
882 .await
883 .map(|c| c.to_bytes())
884 .unwrap_or_else(|_| Bytes::new());
885
886 let mut fw_request = HttpRequest::builder()
887 .method(parts.method)
888 .uri(parts.uri)
889 .version(parts.version)
890 .headers(parts.headers)
891 .body(body_bytes)
892 .build()
893 .expect("Failed to build reinhardt request");
894
895 if let Some(ctx) = &self.handler_di_context {
896 fw_request.set_di_context(Arc::clone(ctx));
897 }
898
899 let fw_response = async_handler
900 .handle(fw_request)
901 .await
902 .unwrap_or_else(HttpResponse::from);
903
904 let mut builder = http::Response::builder().status(fw_response.status);
905 for (key, value) in fw_response.headers.iter() {
906 builder = builder.header(key, value);
907 }
908 builder
909 .body(Full::new(fw_response.body))
910 .expect("Failed to build http::Response")
911 } else if let Some(handler) = &self.handler {
912 handler(request)
914 } else {
915 let (parts, body) = request.into_parts();
917
918 let url = if parts.uri.scheme_str().is_some() {
920 parts.uri.to_string()
922 } else {
923 format!(
925 "{}{}",
926 self.base_url.trim_end_matches('/'),
927 parts.uri.path()
928 )
929 };
930
931 let mut reqwest_request = self.http_client.request(
933 reqwest::Method::from_bytes(parts.method.as_str().as_bytes()).unwrap(),
934 &url,
935 );
936
937 for (name, value) in parts.headers.iter() {
939 if self.use_cookie_store && name.as_str().eq_ignore_ascii_case("cookie") {
940 continue;
941 }
942 reqwest_request = reqwest_request.header(name.as_str(), value.as_bytes());
943 }
944
945 let body_bytes = body
947 .collect()
948 .await
949 .map(|c| c.to_bytes())
950 .unwrap_or_else(|_| Bytes::new());
951 if !body_bytes.is_empty() {
952 reqwest_request = reqwest_request.body(body_bytes.to_vec());
953 }
954
955 let reqwest_response = reqwest_request.send().await?;
957
958 let status = reqwest_response.status();
960 let version = reqwest_response.version();
961 let headers = reqwest_response.headers().clone();
962 let body_bytes = reqwest_response.bytes().await?;
963
964 let mut response_builder = Response::builder().status(status).version(version);
965 for (name, value) in headers.iter() {
966 response_builder = response_builder.header(name, value);
967 }
968
969 response_builder.body(Full::new(body_bytes))?
970 };
971
972 let (parts, response_body) = response.into_parts();
974 let body_data = response_body
975 .collect()
976 .await
977 .map(|collected| collected.to_bytes())
978 .unwrap_or_else(|_| Bytes::new());
979
980 Ok(TestResponse::with_body_and_version(
981 parts.status,
982 parts.headers,
983 body_data,
984 parts.version,
985 ))
986 }
987
988 fn serialize_data<T: Serialize>(&self, data: &T, format: &str) -> ClientResult<Bytes> {
990 match format {
991 "json" => {
992 let json = serde_json::to_vec(data)?;
993 Ok(Bytes::from(json))
994 }
995 "form" => {
996 let json_value = serde_json::to_value(data)?;
998 if let Value::Object(map) = json_value {
999 let form_data = map
1000 .iter()
1001 .map(|(k, v)| {
1002 let value_str = match v {
1003 Value::String(s) => s.clone(),
1004 _ => v.to_string(),
1005 };
1006 format!(
1007 "{}={}",
1008 urlencoding::encode(k),
1009 urlencoding::encode(&value_str)
1010 )
1011 })
1012 .collect::<Vec<_>>()
1013 .join("&");
1014 Ok(Bytes::from(form_data))
1015 } else {
1016 Err(ClientError::RequestFailed(
1017 "Expected object for form data".to_string(),
1018 ))
1019 }
1020 }
1021 _ => Err(ClientError::RequestFailed(format!(
1022 "Unsupported format: {}",
1023 format
1024 ))),
1025 }
1026 }
1027
1028 fn get_content_type(&self, format: &str) -> &str {
1030 match format {
1031 "json" => "application/json",
1032 "form" => "application/x-www-form-urlencoded",
1033 _ => "application/octet-stream",
1034 }
1035 }
1036}
1037
1038fn validate_cookie_key(key: &str) {
1046 assert!(!key.is_empty(), "cookie key must not be empty");
1047 assert!(
1048 !key.contains('='),
1049 "cookie key must not contain '=' (found in key: {:?})",
1050 key
1051 );
1052 assert!(
1053 !key.contains(';'),
1054 "cookie key must not contain ';' (found in key: {:?})",
1055 key
1056 );
1057 assert!(
1058 !key.chars().any(|c| c.is_ascii_whitespace()),
1059 "cookie key must not contain whitespace (found in key: {:?})",
1060 key
1061 );
1062 assert!(
1063 !key.chars().any(|c| c.is_control()),
1064 "cookie key must not contain control characters (found in key: {:?})",
1065 key
1066 );
1067}
1068
1069fn validate_cookie_value(value: &str) {
1077 assert!(!value.contains(';'), "cookie value must not contain ';'");
1078 assert!(
1079 !value.contains('\r') && !value.contains('\n'),
1080 "cookie value must not contain newlines"
1081 );
1082 assert!(
1083 !value.chars().any(|c| c.is_control()),
1084 "cookie value must not contain control characters"
1085 );
1086}
1087
1088impl Default for APIClient {
1089 fn default() -> Self {
1090 Self::new()
1091 }
1092}
1093
1094mod base64 {
1096 pub(super) fn encode(input: String) -> String {
1097 use base64_simd::STANDARD;
1099 STANDARD.encode_to_string(input.as_bytes())
1100 }
1101}
1102
1103mod urlencoding {
1105 pub(super) fn encode(input: &str) -> String {
1106 url::form_urlencoded::byte_serialize(input.as_bytes()).collect()
1107 }
1108}
1109
1110#[cfg(test)]
1111mod tests {
1112 use super::*;
1113 use async_trait::async_trait;
1114 use reinhardt_core::exception::{Error as HttpError, Result as HttpResult};
1115 use rstest::rstest;
1116
1117 struct EchoHandler;
1120
1121 #[async_trait]
1122 impl HttpHandler for EchoHandler {
1123 async fn handle(&self, request: HttpRequest) -> HttpResult<HttpResponse> {
1124 let path = request.uri.path().to_string();
1125 let has_custom = request.headers.get("X-Custom").is_some();
1126 let content_type = request
1127 .headers
1128 .get("Content-Type")
1129 .and_then(|v| v.to_str().ok())
1130 .unwrap_or("")
1131 .to_string();
1132
1133 let mut response = HttpResponse::ok().with_body(path.clone());
1134 response = response.try_with_header("X-Echo-Path", &path)?;
1135
1136 if has_custom {
1137 response = response.try_with_header("X-Echo-Custom", "present")?;
1138 }
1139 if !content_type.is_empty() {
1140 response = response.try_with_header("X-Echo-Content-Type", &content_type)?;
1141 }
1142 Ok(response)
1143 }
1144 }
1145
1146 struct ErrorHandler;
1148
1149 #[async_trait]
1150 impl HttpHandler for ErrorHandler {
1151 async fn handle(&self, _request: HttpRequest) -> HttpResult<HttpResponse> {
1152 Err(HttpError::NotFound("test resource".to_string()))
1153 }
1154 }
1155
1156 #[rstest]
1157 #[tokio::test]
1158 async fn test_from_handler_basic() {
1159 let client = APIClient::from_handler(EchoHandler);
1161
1162 let response = client.get("/test/path/").await.expect("request failed");
1164
1165 assert_eq!(response.status(), http::StatusCode::OK);
1167 assert_eq!(response.body().as_ref(), b"/test/path/");
1168 }
1169
1170 #[rstest]
1171 #[tokio::test]
1172 async fn test_from_handler_post_body() {
1173 let client = APIClient::from_handler(EchoHandler);
1175 let body = serde_json::json!({"key": "value"});
1176
1177 let response = client
1179 .post("/echo/", &body, "json")
1180 .await
1181 .expect("request failed");
1182
1183 assert_eq!(response.status(), http::StatusCode::OK);
1185 assert_eq!(
1186 response
1187 .header("X-Echo-Content-Type")
1188 .expect("missing header"),
1189 "application/json"
1190 );
1191 }
1192
1193 #[rstest]
1194 #[tokio::test]
1195 async fn test_from_handler_headers() {
1196 let client = APIClient::from_handler(EchoHandler);
1198 client
1199 .set_header("X-Custom", "test-value")
1200 .await
1201 .expect("set_header failed");
1202
1203 let response = client.get("/test/").await.expect("request failed");
1205
1206 assert_eq!(response.status(), http::StatusCode::OK);
1208 assert_eq!(
1209 response.header("X-Echo-Custom").expect("missing header"),
1210 "present"
1211 );
1212 }
1213
1214 #[rstest]
1215 #[tokio::test]
1216 async fn test_from_handler_error_conversion() {
1217 let client = APIClient::from_handler(ErrorHandler);
1219
1220 let response = client.get("/anything/").await.expect("request failed");
1222
1223 assert_eq!(response.status(), http::StatusCode::NOT_FOUND);
1225 }
1226
1227 #[rstest]
1228 #[tokio::test]
1229 async fn test_from_handler_origin_header() {
1230 let client = APIClient::from_handler(EchoHandler);
1232
1233 let headers = client.default_headers.read().await;
1235
1236 let origin = headers
1238 .get(http::header::ORIGIN)
1239 .expect("Origin header not set");
1240 assert_eq!(origin.to_str().unwrap(), "http://testserver");
1241 }
1242
1243 #[rstest]
1244 #[tokio::test]
1245 async fn test_builder_with_handler() {
1246 let client = APIClient::builder()
1248 .base_url("http://mytest")
1249 .handler(EchoHandler)
1250 .build();
1251
1252 let response = client.get("/api/").await.expect("request failed");
1254
1255 assert_eq!(response.status(), http::StatusCode::OK);
1257 let headers = client.default_headers.read().await;
1258 let origin = headers
1259 .get(http::header::ORIGIN)
1260 .expect("Origin header not set");
1261 assert_eq!(origin.to_str().unwrap(), "http://mytest");
1262 }
1263
1264 #[rstest]
1265 fn test_validate_cookie_key_accepts_valid_key() {
1266 let key = "session_id";
1268
1269 validate_cookie_key(key);
1271 }
1272
1273 #[rstest]
1274 #[should_panic(expected = "must not be empty")]
1275 fn test_validate_cookie_key_rejects_empty() {
1276 let key = "";
1278
1279 validate_cookie_key(key);
1281 }
1282
1283 #[rstest]
1284 #[should_panic(expected = "must not contain '='")]
1285 fn test_validate_cookie_key_rejects_equals_sign() {
1286 let key = "key=value";
1288
1289 validate_cookie_key(key);
1291 }
1292
1293 #[rstest]
1294 #[should_panic(expected = "must not contain ';'")]
1295 fn test_validate_cookie_key_rejects_semicolon() {
1296 let key = "key;injection";
1298
1299 validate_cookie_key(key);
1301 }
1302
1303 #[rstest]
1304 #[should_panic(expected = "must not contain whitespace")]
1305 fn test_validate_cookie_key_rejects_whitespace() {
1306 let key = "key name";
1308
1309 validate_cookie_key(key);
1311 }
1312
1313 #[rstest]
1314 #[should_panic(expected = "must not contain control characters")]
1315 fn test_validate_cookie_key_rejects_control_chars() {
1316 let key = "key\x00name";
1318
1319 validate_cookie_key(key);
1321 }
1322
1323 #[rstest]
1324 fn test_validate_cookie_value_accepts_valid_value() {
1325 let value = "abc123-token";
1327
1328 validate_cookie_value(value);
1330 }
1331
1332 #[rstest]
1333 fn test_validate_cookie_value_accepts_empty() {
1334 let value = "";
1336
1337 validate_cookie_value(value);
1339 }
1340
1341 #[rstest]
1342 #[should_panic(expected = "must not contain ';'")]
1343 fn test_validate_cookie_value_rejects_semicolon() {
1344 let value = "value; extra=injected";
1346
1347 validate_cookie_value(value);
1349 }
1350
1351 #[rstest]
1352 #[should_panic(expected = "must not contain newlines")]
1353 fn test_validate_cookie_value_rejects_newline() {
1354 let value = "value\r\nInjected-Header: malicious";
1356
1357 validate_cookie_value(value);
1359 }
1360
1361 #[rstest]
1362 #[should_panic(expected = "must not contain control characters")]
1363 fn test_validate_cookie_value_rejects_control_chars() {
1364 let value = "value\x01hidden";
1366
1367 validate_cookie_value(value);
1369 }
1370
1371 #[rstest]
1372 #[should_panic(expected = "must not contain newlines")]
1373 fn test_validate_cookie_value_rejects_lf_only() {
1374 let value = "value\nInjected-Header: evil";
1376
1377 validate_cookie_value(value);
1379 }
1380}