pub mod cache_control;
pub mod compile;
pub mod errors;
pub mod expression;
pub mod plan;
pub mod request;
pub mod response;
pub mod sanitizer;
#[cfg(test)]
mod tests {
use crate::config::parse_yaml_config;
use crate::executor::{
execution::client_request_details::{
ClientRequestDetails, JwtRequestDetails, OperationDetails,
},
headers::{
compile::compile_headers_plan,
request::modify_subgraph_request_headers,
response::{apply_subgraph_response_headers, ResponseHeaderAggregator},
},
};
use http::{HeaderMap, HeaderName, HeaderValue};
use ntex::http::HeaderMap as NtexHeaderMap;
fn header_name_owned(s: &str) -> HeaderName {
HeaderName::from_bytes(s.as_bytes()).unwrap()
}
fn header_value_owned(s: &str) -> HeaderValue {
HeaderValue::from_str(s).unwrap()
}
trait HeaderMapAsStringExt {
fn to_string(&self) -> String;
}
impl HeaderMapAsStringExt for HeaderMap {
fn to_string(&self) -> String {
let mut buffer = String::new();
for (name, value) in self.iter() {
buffer.push_str(&format!(
"{}: {}\n",
name.as_str(),
value.to_str().unwrap_or("<invalid utf8>")
));
}
buffer
}
}
impl HeaderMapAsStringExt for ntex::http::HeaderMap {
fn to_string(&self) -> String {
let mut buffer = String::new();
for (name, value) in self.iter() {
buffer.push_str(&format!(
"{}: {}\n",
name.as_str(),
value.to_str().unwrap_or("<invalid utf8>")
));
}
buffer
}
}
#[test]
fn test_build_subgraph_headers_propagate_and_set() {
let yaml_str = r#"
headers:
all:
request:
- propagate:
named: x-prop
rename: x-renamed
- insert:
name: x-set
value: set-value
"#;
let config = parse_yaml_config(String::from(yaml_str)).unwrap();
let plan = compile_headers_plan(&config.headers).unwrap();
let mut client_headers = NtexHeaderMap::new();
client_headers.insert(
header_name_owned("x-prop"),
header_value_owned("abc").into(),
);
let client_details = ClientRequestDetails {
method: &http::Method::POST,
url: &"http://example.com".parse().unwrap(),
headers: client_headers.into(),
operation: OperationDetails {
name: None,
query: "{ __typename }",
kind: "query",
},
jwt: JwtRequestDetails::Unauthenticated.into(),
path_params: Default::default(),
};
let mut out = HeaderMap::new();
modify_subgraph_request_headers(&plan, "any", &client_details, &mut out).unwrap();
insta::assert_snapshot!(out.to_string(), @r#"
x-renamed: abc
x-set: set-value
"#);
}
#[test]
fn test_build_subgraph_headers_with_default() {
let yaml_str = r#"
headers:
all:
request:
- propagate:
named: x-missing
default: default-value
"#;
let config = parse_yaml_config(String::from(yaml_str)).unwrap();
let plan = compile_headers_plan(&config.headers).unwrap();
let client_headers = NtexHeaderMap::new();
let client_details = ClientRequestDetails {
method: &http::Method::POST,
url: &"http://example.com".parse().unwrap(),
headers: client_headers.into(),
operation: OperationDetails {
name: None,
query: "{ __typename }",
kind: "query",
},
jwt: JwtRequestDetails::Unauthenticated.into(),
path_params: Default::default(),
};
let mut out = HeaderMap::new();
modify_subgraph_request_headers(&plan, "any", &client_details, &mut out).unwrap();
insta::assert_snapshot!(out.to_string(), @r#"
x-missing: default-value
"#);
}
#[test]
fn test_propagate_with_matching_and_exclude() {
let yaml_str = r#"
headers:
all:
request:
- propagate:
matching: "^x-.*"
exclude: ["^x-secret-.*"]
"#;
let config = parse_yaml_config(String::from(yaml_str)).unwrap();
let plan = compile_headers_plan(&config.headers).unwrap();
let mut client_headers = NtexHeaderMap::new();
client_headers.insert(
header_name_owned("x-forward-this"),
header_value_owned("value1").into(),
);
client_headers.insert(
header_name_owned("x-secret-header"),
header_value_owned("value2").into(),
);
client_headers.insert(
header_name_owned("authorization"),
header_value_owned("value3").into(),
);
let client_details = ClientRequestDetails {
method: &http::Method::POST,
url: &"http://example.com".parse().unwrap(),
headers: client_headers.into(),
operation: OperationDetails {
name: None,
query: "{ __typename }",
kind: "query",
},
jwt: JwtRequestDetails::Unauthenticated.into(),
path_params: Default::default(),
};
let mut out = HeaderMap::new();
modify_subgraph_request_headers(&plan, "any", &client_details, &mut out).unwrap();
assert_eq!(out.get("x-forward-this").unwrap(), "value1");
assert!(out.get("x-secret-header").is_none());
assert!(out.get("authorization").is_none());
insta::assert_snapshot!(out.to_string(), @r#"
x-forward-this: value1
"#);
}
#[test]
fn test_insert_request_header_with_expression() {
let yaml_str = r#"
headers:
all:
request:
- insert:
name: x-operation-name
expression: '.request.operation.name || "unknown"'
"#;
let config = parse_yaml_config(String::from(yaml_str)).unwrap();
let plan = compile_headers_plan(&config.headers).unwrap();
let client_headers = NtexHeaderMap::new();
let client_details = ClientRequestDetails {
method: &http::Method::POST,
url: &"http://example.com".parse().unwrap(),
headers: client_headers.into(),
operation: OperationDetails {
name: Some("MyQuery"),
query: "{ __typename }",
kind: "query",
},
jwt: JwtRequestDetails::Unauthenticated.into(),
path_params: Default::default(),
};
let mut out = HeaderMap::new();
modify_subgraph_request_headers(&plan, "any", &client_details, &mut out).unwrap();
insta::assert_snapshot!(out.to_string(), @r#"
x-operation-name: MyQuery
"#);
}
#[test]
fn test_insert_request_header_with_expression_fallback() {
let yaml_str = r#"
headers:
all:
request:
- insert:
name: x-operation-name
expression: '.request.operation.name || "unknown"'
"#;
let config = parse_yaml_config(String::from(yaml_str)).unwrap();
let plan = compile_headers_plan(&config.headers).unwrap();
let client_headers = NtexHeaderMap::new();
let client_details = ClientRequestDetails {
method: &http::Method::POST,
url: &"http://example.com".parse().unwrap(),
headers: client_headers.into(),
operation: OperationDetails {
name: None,
query: "{ __typename }",
kind: "query",
},
jwt: JwtRequestDetails::Unauthenticated.into(),
path_params: Default::default(),
};
let mut out = HeaderMap::new();
modify_subgraph_request_headers(&plan, "any", &client_details, &mut out).unwrap();
insta::assert_snapshot!(out.to_string(), @r#"
x-operation-name: unknown
"#);
}
#[test]
fn test_subgraph_specific_request_rules() {
let yaml_str = r#"
headers:
all:
request:
- insert:
name: x-scope
value: all
subgraphs:
accounts:
request:
- insert:
name: x-scope
value: accounts
"#;
let config = parse_yaml_config(String::from(yaml_str)).unwrap();
let plan = compile_headers_plan(&config.headers).unwrap();
let client_headers = NtexHeaderMap::new();
let client_details = ClientRequestDetails {
method: &http::Method::POST,
url: &"http://example.com".parse().unwrap(),
headers: client_headers.into(),
operation: OperationDetails {
name: None,
query: "{ __typename }",
kind: "query",
},
jwt: JwtRequestDetails::Unauthenticated.into(),
path_params: Default::default(),
};
let mut out_accounts = HeaderMap::new();
modify_subgraph_request_headers(&plan, "accounts", &client_details, &mut out_accounts)
.unwrap();
insta::assert_snapshot!(out_accounts.to_string(), @r#"
x-scope: accounts
"#);
let mut out_other = HeaderMap::new();
modify_subgraph_request_headers(&plan, "products", &client_details, &mut out_other)
.unwrap();
insta::assert_snapshot!(out_other.to_string(), @r#"
x-scope: all
"#);
}
#[test]
fn test_apply_subgraph_response_headers_and_finalize() {
let yaml_str = r#"
headers:
all:
response:
- propagate:
named: x-resp
algorithm: last
"#;
let config = parse_yaml_config(String::from(yaml_str)).unwrap();
let plan = compile_headers_plan(&config.headers).unwrap();
let client_headers = NtexHeaderMap::new();
let client_details = ClientRequestDetails {
method: &http::Method::POST,
url: &"http://example.com".parse().unwrap(),
headers: client_headers.into(),
operation: OperationDetails {
name: None,
query: "{ __typename }",
kind: "query",
},
jwt: JwtRequestDetails::Unauthenticated.into(),
path_params: Default::default(),
};
let mut accumulator = ResponseHeaderAggregator::default();
let mut subgraph_headers = HeaderMap::new();
subgraph_headers.insert(
header_name_owned("x-resp"),
header_value_owned("resp-value-1"),
);
apply_subgraph_response_headers(
&plan,
"any",
&subgraph_headers,
&client_details,
&mut accumulator,
)
.unwrap();
let mut subgraph_headers = HeaderMap::new();
subgraph_headers.insert(
header_name_owned("x-resp"),
header_value_owned("resp-value-2"),
);
apply_subgraph_response_headers(
&plan,
"any",
&subgraph_headers,
&client_details,
&mut accumulator,
)
.unwrap();
let mut response = ntex::http::Response::Ok().finish();
accumulator
.modify_client_response_headers(response.headers_mut())
.unwrap();
let final_headers = response.headers();
insta::assert_snapshot!(final_headers.to_string(), @r#"
x-resp: resp-value-2
"#);
}
#[test]
fn test_response_propagate_first() {
let yaml_str = r#"
headers:
all:
response:
- propagate:
named: x-resp
algorithm: first
"#;
let config = parse_yaml_config(String::from(yaml_str)).unwrap();
let plan = compile_headers_plan(&config.headers).unwrap();
let client_headers = NtexHeaderMap::new();
let client_details = ClientRequestDetails {
method: &http::Method::POST,
url: &"http://example.com".parse().unwrap(),
headers: client_headers.into(),
operation: OperationDetails {
name: None,
query: "{ __typename }",
kind: "query",
},
jwt: JwtRequestDetails::Unauthenticated.into(),
path_params: Default::default(),
};
let mut accumulator = ResponseHeaderAggregator::default();
let mut subgraph_headers_1 = HeaderMap::new();
subgraph_headers_1.insert(
header_name_owned("x-resp"),
header_value_owned("resp-value-1"),
);
apply_subgraph_response_headers(
&plan,
"any",
&subgraph_headers_1,
&client_details,
&mut accumulator,
)
.unwrap();
let mut subgraph_headers_2 = HeaderMap::new();
subgraph_headers_2.insert(
header_name_owned("x-resp"),
header_value_owned("resp-value-2"),
);
apply_subgraph_response_headers(
&plan,
"any",
&subgraph_headers_2,
&client_details,
&mut accumulator,
)
.unwrap();
let mut response = ntex::http::Response::Ok().finish();
accumulator
.modify_client_response_headers(response.headers_mut())
.unwrap();
let final_headers = response.headers();
insta::assert_snapshot!(final_headers.to_string(), @r#"
x-resp: resp-value-1
"#);
}
#[test]
fn test_response_propagate_append() {
let yaml_str = r#"
headers:
all:
response:
- propagate:
named: x-stuff
algorithm: append
"#;
let config = parse_yaml_config(String::from(yaml_str)).unwrap();
let plan = compile_headers_plan(&config.headers).unwrap();
let client_headers = NtexHeaderMap::new();
let client_details = ClientRequestDetails {
method: &http::Method::POST,
url: &"http://example.com".parse().unwrap(),
headers: client_headers.into(),
operation: OperationDetails {
name: None,
query: "{ __typename }",
kind: "query",
},
jwt: JwtRequestDetails::Unauthenticated.into(),
path_params: Default::default(),
};
let mut accumulator = ResponseHeaderAggregator::default();
let mut subgraph1_headers = HeaderMap::new();
subgraph1_headers.insert(header_name_owned("x-stuff"), header_value_owned("val1"));
apply_subgraph_response_headers(
&plan,
"subgraph1",
&subgraph1_headers,
&client_details,
&mut accumulator,
)
.unwrap();
let mut subgraph2_headers = HeaderMap::new();
subgraph2_headers.insert(header_name_owned("x-stuff"), header_value_owned("val2"));
apply_subgraph_response_headers(
&plan,
"subgraph2",
&subgraph2_headers,
&client_details,
&mut accumulator,
)
.unwrap();
let mut response = ntex::http::Response::Ok().finish();
accumulator
.modify_client_response_headers(response.headers_mut())
.unwrap();
let final_headers = response.headers();
insta::assert_snapshot!(final_headers.to_string(), @r#"
x-stuff: val1, val2
"#);
}
#[test]
fn test_response_propagate_append_never_join() {
let yaml_str = r#"
headers:
all:
response:
- propagate:
named: set-cookie
algorithm: append
"#;
let config = parse_yaml_config(String::from(yaml_str)).unwrap();
let plan = compile_headers_plan(&config.headers).unwrap();
let client_headers = NtexHeaderMap::new();
let client_details = ClientRequestDetails {
method: &http::Method::POST,
url: &"http://example.com".parse().unwrap(),
headers: client_headers.into(),
operation: OperationDetails {
name: None,
query: "{ __typename }",
kind: "query",
},
jwt: JwtRequestDetails::Unauthenticated.into(),
path_params: Default::default(),
};
let mut accumulator = ResponseHeaderAggregator::default();
let mut subgraph1_headers = HeaderMap::new();
subgraph1_headers.insert(header_name_owned("set-cookie"), header_value_owned("a=1"));
apply_subgraph_response_headers(
&plan,
"subgraph1",
&subgraph1_headers,
&client_details,
&mut accumulator,
)
.unwrap();
let mut subgraph2_headers = HeaderMap::new();
subgraph2_headers.insert(header_name_owned("set-cookie"), header_value_owned("b=2"));
apply_subgraph_response_headers(
&plan,
"subgraph2",
&subgraph2_headers,
&client_details,
&mut accumulator,
)
.unwrap();
let mut response = ntex::http::Response::Ok().finish();
accumulator
.modify_client_response_headers(response.headers_mut())
.unwrap();
let final_headers = response.headers();
insta::assert_snapshot!(final_headers.to_string(), @r#"
set-cookie: a=1
set-cookie: b=2
"#);
}
#[test]
fn test_insert_response_header_with_expression() {
let yaml_str = r#"
headers:
all:
response:
- insert:
name: x-original-forwarded-for
expression: '.response.headers."x-forwarded-for"'
"#;
let config = parse_yaml_config(String::from(yaml_str)).unwrap();
let plan = compile_headers_plan(&config.headers).unwrap();
let client_headers = NtexHeaderMap::new();
let client_details = ClientRequestDetails {
method: &http::Method::POST,
url: &"http://example.com".parse().unwrap(),
headers: client_headers.into(),
operation: OperationDetails {
name: None,
query: "{ __typename }",
kind: "query",
},
jwt: JwtRequestDetails::Unauthenticated.into(),
path_params: Default::default(),
};
let mut accumulator = ResponseHeaderAggregator::default();
let mut subgraph_headers = HeaderMap::new();
subgraph_headers.insert(
header_name_owned("x-forwarded-for"),
header_value_owned("1.2.3.4"),
);
apply_subgraph_response_headers(
&plan,
"any",
&subgraph_headers,
&client_details,
&mut accumulator,
)
.unwrap();
let mut response = ntex::http::Response::Ok().finish();
accumulator
.modify_client_response_headers(response.headers_mut())
.unwrap();
let final_headers = response.headers();
insta::assert_snapshot!(final_headers.to_string(), @r#"
x-original-forwarded-for: 1.2.3.4
"#);
}
#[test]
fn test_remove_header() {
let yaml_str = r#"
headers:
all:
request:
- propagate:
named: x-keep
- remove:
named: x-remove
"#;
let config = parse_yaml_config(String::from(yaml_str)).unwrap();
let plan = compile_headers_plan(&config.headers).unwrap();
let mut client_headers = NtexHeaderMap::new();
client_headers.insert(
header_name_owned("x-remove"),
header_value_owned("bye").into(),
);
client_headers.insert(header_name_owned("x-keep"), header_value_owned("hi").into());
let client_details = ClientRequestDetails {
method: &http::Method::POST,
url: &"http://example.com".parse().unwrap(),
headers: client_headers.into(),
operation: OperationDetails {
name: None,
query: "{ __typename }",
kind: "query",
},
jwt: JwtRequestDetails::Unauthenticated.into(),
path_params: Default::default(),
};
let mut out = HeaderMap::new();
modify_subgraph_request_headers(&plan, "any", &client_details, &mut out).unwrap();
insta::assert_snapshot!(out.to_string(), @r#"
x-keep: hi
"#);
}
}