use std::str::FromStr;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::SystemTime;
use http::HeaderMap;
use http::HeaderValue;
use http::Method;
use http::StatusCode;
use rhai::Engine;
use rhai::EvalAltResult;
use serde_json::Value;
use sha2::Digest;
use tower::BoxError;
use tower::Service;
use tower::ServiceExt;
use tower::util::BoxService;
use uuid::Uuid;
use super::PathBuf;
use super::Rhai;
use super::process_error;
use super::subgraph;
use crate::Context;
use crate::graphql;
use crate::graphql::Error;
use crate::graphql::Request;
use crate::http_ext;
use crate::plugin::DynPlugin;
use crate::plugin::test::MockExecutionService;
use crate::plugin::test::MockSupergraphService;
use crate::plugins::rhai::engine::RhaiExecutionDeferredResponse;
use crate::plugins::rhai::engine::RhaiExecutionResponse;
use crate::plugins::rhai::engine::RhaiRouterChunkedResponse;
use crate::plugins::rhai::engine::RhaiRouterFirstRequest;
use crate::plugins::rhai::engine::RhaiRouterResponse;
use crate::plugins::rhai::engine::RhaiSupergraphDeferredResponse;
use crate::plugins::rhai::engine::RhaiSupergraphResponse;
use crate::services::ExecutionRequest;
use crate::services::SubgraphRequest;
use crate::services::SupergraphRequest;
use crate::services::SupergraphResponse;
async fn call_rhai_function(fn_name: &str) -> Result<(), Box<rhai::EvalAltResult>> {
let dyn_plugin: Box<dyn DynPlugin> = crate::plugin::plugins()
.find(|factory| factory.name == "apollo.rhai")
.expect("Plugin not found")
.create_instance_without_schema(
&Value::from_str(
r#"{"scripts":"tests/fixtures", "main":"request_response_test.rhai"}"#,
)
.unwrap(),
)
.await
.unwrap();
let it: &dyn std::any::Any = dyn_plugin.as_any();
let rhai_instance: &Rhai = it.downcast_ref::<Rhai>().expect("downcast");
let block = rhai_instance.block.load();
let scope = block.scope.clone();
let mut guard = scope.lock().unwrap();
let response = Arc::new(Mutex::new(Some(subgraph::Response::fake_builder().build())));
block
.engine
.call_fn(&mut guard, &block.ast, fn_name, (response,))
}
async fn call_rhai_function_with_arg<T: Sync + Send + 'static>(
fn_name: &str,
arg: T,
) -> Result<(), Box<rhai::EvalAltResult>> {
let dyn_plugin: Box<dyn DynPlugin> = crate::plugin::plugins()
.find(|factory| factory.name == "apollo.rhai")
.expect("Plugin not found")
.create_instance_without_schema(
&Value::from_str(
r#"{"scripts":"tests/fixtures", "main":"request_response_test.rhai"}"#,
)
.unwrap(),
)
.await
.unwrap();
let it: &dyn std::any::Any = dyn_plugin.as_any();
let rhai_instance: &Rhai = it.downcast_ref::<Rhai>().expect("downcast");
let block = rhai_instance.block.load();
let scope = block.scope.clone();
let mut guard = scope.lock().unwrap();
let wrapped_arg = Arc::new(Mutex::new(Some(arg)));
block
.engine
.call_fn(&mut guard, &block.ast, fn_name, (wrapped_arg,))
}
#[tokio::test]
async fn rhai_plugin_supergraph_service() -> Result<(), BoxError> {
let mut mock_service = MockSupergraphService::new();
mock_service
.expect_call()
.times(1)
.returning(move |req: SupergraphRequest| {
Ok(SupergraphResponse::fake_builder()
.header("x-custom-header", "CUSTOM_VALUE")
.context(req.context)
.build()
.unwrap())
});
let dyn_plugin: Box<dyn DynPlugin> = crate::plugin::plugins()
.find(|factory| factory.name == "apollo.rhai")
.expect("Plugin not found")
.create_instance_without_schema(
&Value::from_str(r#"{"scripts":"tests/fixtures", "main":"test.rhai"}"#).unwrap(),
)
.await
.unwrap();
let mut router_service = dyn_plugin.supergraph_service(BoxService::new(mock_service));
let context = Context::new();
context.insert("test", 5i64).unwrap();
let supergraph_req = SupergraphRequest::fake_builder().context(context).build()?;
let mut supergraph_resp = router_service.ready().await?.call(supergraph_req).await?;
assert_eq!(supergraph_resp.response.status(), 200);
let headers = supergraph_resp.response.headers().clone();
let context = supergraph_resp.context.clone();
let resp = supergraph_resp.next_response().await.unwrap();
if !resp.errors.is_empty() {
panic!(
"Contains errors : {}",
resp.errors
.into_iter()
.map(|err| err.to_string())
.collect::<Vec<String>>()
.join("\n")
);
}
assert_eq!(headers.get("coucou").unwrap(), &"hello");
assert_eq!(headers.get("coming_from_entries").unwrap(), &"value_15");
assert_eq!(context.get::<_, i64>("test").unwrap().unwrap(), 42i64);
assert_eq!(
context.get::<_, String>("addition").unwrap().unwrap(),
"Here is a new element in the context".to_string()
);
Ok(())
}
#[tokio::test]
async fn rhai_plugin_execution_service_error() -> Result<(), BoxError> {
let mut mock_service = MockExecutionService::new();
mock_service.expect_clone().return_once(move || {
let mut mock_service = MockExecutionService::new();
mock_service.expect_call().never();
mock_service
});
let dyn_plugin: Box<dyn DynPlugin> = crate::plugin::plugins()
.find(|factory| factory.name == "apollo.rhai")
.expect("Plugin not found")
.create_instance_without_schema(
&Value::from_str(r#"{"scripts":"tests/fixtures", "main":"test.rhai"}"#).unwrap(),
)
.await
.unwrap();
let mut router_service = dyn_plugin.execution_service(BoxService::new(mock_service));
let fake_req = http_ext::Request::fake_builder()
.header("x-custom-header", "CUSTOM_VALUE")
.body(Request::builder().query(String::new()).build())
.build()?;
let context = Context::new();
context.insert("test", 5i64).unwrap();
let exec_req = ExecutionRequest::fake_builder()
.context(context)
.supergraph_request(fake_req)
.build();
let mut exec_resp = router_service
.ready()
.await
.unwrap()
.call(exec_req)
.await
.unwrap();
assert_eq!(
exec_resp.response.status(),
http::StatusCode::INTERNAL_SERVER_ERROR
);
let body = exec_resp.next_response().await.unwrap();
if body.errors.is_empty() {
panic!(
"Must contain errors : {}",
body.errors
.into_iter()
.map(|err| err.to_string())
.collect::<Vec<String>>()
.join("\n")
);
}
assert_eq!(
body.errors.first().unwrap().message.as_str(),
"rhai execution error: 'Runtime error: An error occured (line 30, position 5)'"
);
Ok(())
}
fn new_rhai_test_engine() -> Engine {
Rhai::new_rhai_engine(None, "".to_string(), PathBuf::new())
}
#[test]
fn it_logs_messages() {
let env_filter = "apollo_router=trace";
let mock_writer = tracing_test::internal::MockWriter::new(tracing_test::internal::global_buf());
let subscriber = tracing_test::internal::get_subscriber(mock_writer, env_filter);
let _guard = tracing::dispatcher::set_default(&subscriber);
let engine = new_rhai_test_engine();
let input_logs = vec![
r#"log_trace("trace log")"#,
r#"log_debug("debug log")"#,
r#"log_info("info log")"#,
r#"log_warn("warn log")"#,
r#"log_error("error log")"#,
];
for log in input_logs {
engine.eval::<()>(log).expect("it logged a message");
}
assert!(tracing_test::internal::logs_with_scope_contain(
"apollo_router",
"trace log"
));
assert!(tracing_test::internal::logs_with_scope_contain(
"apollo_router",
"debug log"
));
assert!(tracing_test::internal::logs_with_scope_contain(
"apollo_router",
"info log"
));
assert!(tracing_test::internal::logs_with_scope_contain(
"apollo_router",
"warn log"
));
assert!(tracing_test::internal::logs_with_scope_contain(
"apollo_router",
"error log"
));
}
#[test]
fn it_prints_messages_to_log() {
use tracing::subscriber;
use crate::assert_snapshot_subscriber;
subscriber::with_default(assert_snapshot_subscriber!(), || {
let engine = new_rhai_test_engine();
engine
.eval::<()>(r#"print("info log")"#)
.expect("it logged a message");
});
}
#[tokio::test]
async fn it_can_access_sdl_constant() {
let dyn_plugin: Box<dyn DynPlugin> = crate::plugin::plugins()
.find(|factory| factory.name == "apollo.rhai")
.expect("Plugin not found")
.create_instance_without_schema(
&Value::from_str(r#"{"scripts":"tests/fixtures", "main":"test.rhai"}"#).unwrap(),
)
.await
.unwrap();
let it: &dyn std::any::Any = dyn_plugin.as_any();
let rhai_instance: &Rhai = it.downcast_ref::<Rhai>().expect("downcast");
let block = rhai_instance.block.load();
let scope = block.scope.clone();
let mut guard = scope.lock().unwrap();
let sdl: String = block
.engine
.call_fn(&mut guard, &block.ast, "get_sdl", ())
.expect("can get sdl");
assert_eq!(sdl.as_str(), "");
}
#[test]
fn it_provides_helpful_headermap_errors() {
let mut engine = new_rhai_test_engine();
engine.register_fn("new_hm", HeaderMap::new);
let result = engine.eval::<HeaderMap>(
r#"
let map = new_hm();
map["ümlaut"] = "will fail";
map
"#,
);
assert!(result.is_err());
assert!(matches!(
*result.unwrap_err(),
EvalAltResult::ErrorRuntime(..)
));
}
#[tokio::test]
async fn it_can_process_router_request() {
let mut request = RhaiRouterFirstRequest::default();
request.request.headers_mut().insert(
"content-type",
HeaderValue::from_str("application/json").unwrap(),
);
*request.request.method_mut() = http::Method::GET;
call_rhai_function_with_arg("process_router_request", request)
.await
.expect("test failed");
}
#[tokio::test]
async fn it_can_process_supergraph_request() {
let request = SupergraphRequest::canned_builder()
.operation_name("canned")
.build()
.expect("build canned supergraph request");
call_rhai_function_with_arg("process_supergraph_request", request)
.await
.expect("test failed");
}
#[tokio::test]
async fn it_can_process_execution_request() {
let request = ExecutionRequest::fake_builder().build();
call_rhai_function_with_arg("process_execution_request", request)
.await
.expect("test failed");
}
#[tokio::test]
async fn it_can_process_subgraph_request() {
let request = SubgraphRequest::fake_builder().build();
call_rhai_function_with_arg("process_subgraph_request", request)
.await
.expect("test failed");
}
#[tokio::test]
async fn it_can_process_router_response() {
let response = RhaiRouterResponse::default();
call_rhai_function_with_arg("process_router_response", response)
.await
.expect("test failed");
}
#[tokio::test]
async fn it_can_process_router_chunked_response() {
let response = RhaiRouterChunkedResponse::default();
call_rhai_function_with_arg("process_router_chunked_response", response)
.await
.expect("test failed");
}
#[tokio::test]
async fn it_can_process_supergraph_response() {
let response = RhaiSupergraphResponse::default();
call_rhai_function_with_arg("process_supergraph_response", response)
.await
.expect("test failed");
}
#[tokio::test]
async fn it_can_process_supergraph_deferred_response() {
let response = RhaiSupergraphDeferredResponse::default();
call_rhai_function_with_arg("process_supergraph_deferred_response", response)
.await
.expect("test failed");
}
#[tokio::test]
async fn it_can_process_execution_response() {
let response = RhaiExecutionResponse::default();
call_rhai_function_with_arg("process_execution_response", response)
.await
.expect("test failed");
}
#[tokio::test]
async fn it_can_process_execution_deferred_response() {
let response = RhaiExecutionDeferredResponse::default();
call_rhai_function_with_arg("process_execution_deferred_response", response)
.await
.expect("test failed");
}
#[tokio::test]
async fn it_can_process_subgraph_response() {
let response = subgraph::Response::fake_builder()
.status_code(StatusCode::OK)
.build();
call_rhai_function_with_arg("process_subgraph_response", response)
.await
.expect("test failed");
}
#[tokio::test]
async fn it_can_parse_request_uri() {
let mut request = SupergraphRequest::canned_builder()
.operation_name("canned")
.build()
.expect("build canned supergraph request");
*request.supergraph_request.uri_mut() = "https://not-default:8080/path".parse().unwrap();
call_rhai_function_with_arg("test_parse_request_details", request)
.await
.expect("test failed");
}
#[test]
fn it_can_urlencode_string() {
let engine = new_rhai_test_engine();
let encoded: String = engine
.eval(r#"urlencode("This has an ümlaut in it.")"#)
.expect("can encode string");
assert_eq!(encoded, "This%20has%20an%20%C3%BCmlaut%20in%20it.");
}
#[test]
fn it_can_urldecode_string() {
let engine = new_rhai_test_engine();
let decoded: String = engine
.eval(r#"urldecode("This%20has%20an%20%C3%BCmlaut%20in%20it.")"#)
.expect("can decode string");
assert_eq!(decoded, "This has an ümlaut in it.");
}
#[test]
fn it_can_base64encode_string() {
let engine = new_rhai_test_engine();
let encoded: String = engine
.eval(r#"base64::encode("This has an ümlaut in it.")"#)
.expect("can encode string");
assert_eq!(encoded, "VGhpcyBoYXMgYW4gw7xtbGF1dCBpbiBpdC4=");
}
#[test]
fn it_can_base64decode_string() {
let engine = new_rhai_test_engine();
let decoded: String = engine
.eval(r#"base64::decode("VGhpcyBoYXMgYW4gw7xtbGF1dCBpbiBpdC4=")"#)
.expect("can decode string");
assert_eq!(decoded, "This has an ümlaut in it.");
}
#[test]
fn it_can_base64encode_string_with_alphabet() {
let engine = new_rhai_test_engine();
let encoded: String = engine
.eval(r#"base64::encode("<<???>>", base64::STANDARD)"#)
.expect("can encode string");
assert_eq!(encoded, "PDw/Pz8+Pg==");
let encoded: String = engine
.eval(r#"base64::encode("<<???>>", base64::URL_SAFE)"#)
.expect("can encode string");
assert_eq!(encoded, "PDw_Pz8-Pg==");
}
#[test]
fn it_can_base64decode_string_with_alphabet() {
let engine = new_rhai_test_engine();
let decoded: String = engine
.eval(r#"base64::decode("PDw/Pz8+Pg==", base64::STANDARD)"#)
.expect("can decode string");
assert_eq!(decoded, "<<???>>");
let decoded: String = engine
.eval(r#"base64::decode("PDw_Pz8-Pg==", base64::URL_SAFE)"#)
.expect("can decode string");
assert_eq!(decoded, "<<???>>");
}
#[test]
fn it_can_create_unix_now() {
let engine = new_rhai_test_engine();
let st = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("can get system time")
.as_secs() as i64;
let unix_now: i64 = engine
.eval(r#"unix_now()"#)
.expect("can get unix_now() timestamp");
assert!(st <= unix_now && unix_now <= st + 1);
}
#[test]
fn it_can_create_unix_ms_now() {
let engine = new_rhai_test_engine();
let st = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("can get system time")
.as_millis() as i64;
let unix_ms_now: i64 = engine
.eval(r#"unix_ms_now()"#)
.expect("can get unix_ms_now() timestamp");
assert!(st <= unix_ms_now && unix_ms_now <= st + 1000);
}
#[test]
fn it_can_generate_uuid() {
let engine = new_rhai_test_engine();
let uuid_v4_rhai: String = engine.eval(r#"uuid_v4()"#).expect("can get uuid");
let uuid_parsed = Uuid::parse_str(uuid_v4_rhai.as_str()).expect("can parse uuid from string");
assert_eq!(uuid_v4_rhai, uuid_parsed.to_string());
}
#[test]
fn it_can_sha256_string() {
let engine = new_rhai_test_engine();
let hash = sha2::Sha256::digest("hello world".as_bytes());
let hash_rhai: String = engine
.eval(r#"sha256::digest("hello world")"#)
.expect("can decode string");
assert_eq!(hash_rhai, hex::encode(hash));
}
async fn base_globals_function(fn_name: &str) -> Result<bool, Box<rhai::EvalAltResult>> {
let dyn_plugin: Box<dyn DynPlugin> = crate::plugin::plugins()
.find(|factory| factory.name == "apollo.rhai")
.expect("Plugin not found")
.create_instance_without_schema(
&Value::from_str(
r#"{"scripts":"tests/fixtures", "main":"global_variables_test.rhai"}"#,
)
.unwrap(),
)
.await
.unwrap();
let it: &dyn std::any::Any = dyn_plugin.as_any();
let rhai_instance: &Rhai = it.downcast_ref::<Rhai>().expect("downcast");
let block = rhai_instance.block.load();
let scope = block.scope.clone();
let mut guard = scope.lock().unwrap();
block.engine.call_fn(&mut guard, &block.ast, fn_name, ())
}
#[tokio::test]
async fn it_can_find_router_global_variables() {
if let Err(error) = base_globals_function("process_router_global_variables").await {
panic!("test failed: {error:?}");
}
}
#[tokio::test]
async fn it_can_process_om_subgraph_forbidden() {
if let Err(error) = call_rhai_function("process_subgraph_response_om_forbidden").await {
let processed_error = process_error(error);
assert_eq!(processed_error.status, StatusCode::FORBIDDEN);
assert_eq!(
processed_error.message,
Some("I have raised a 403".to_string())
);
} else {
panic!("error processed incorrectly");
}
}
#[tokio::test]
async fn it_can_process_om_subgraph_forbidden_with_graphql_payload() {
let error = call_rhai_function("process_subgraph_response_om_forbidden_graphql")
.await
.unwrap_err();
let processed_error = process_error(error);
assert_eq!(processed_error.status, StatusCode::FORBIDDEN);
assert_eq!(
processed_error.body,
Some(
graphql::Response::builder()
.errors(vec![{
Error::builder()
.message("I have raised a 403")
.extension_code("ACCESS_DENIED")
.build()
}])
.build()
)
);
}
#[tokio::test]
async fn it_can_process_om_subgraph_200_with_graphql_data() {
let error = call_rhai_function("process_subgraph_response_om_200_graphql")
.await
.unwrap_err();
let processed_error = process_error(error);
assert_eq!(processed_error.status, StatusCode::OK);
assert_eq!(
processed_error.body,
Some(
graphql::Response::builder()
.data(serde_json::json!({ "name": "Ada Lovelace"}))
.build()
)
);
}
#[tokio::test]
async fn it_can_process_string_subgraph_forbidden() {
if let Err(error) = call_rhai_function("process_subgraph_response_string").await {
let processed_error = process_error(error);
assert_eq!(processed_error.status, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(processed_error.message, Some("rhai execution error: 'Runtime error: I have raised an error (line 251, position 5)'".to_string()));
} else {
panic!("error processed incorrectly");
}
}
#[tokio::test]
async fn it_can_process_ok_subgraph_forbidden() {
let error = call_rhai_function("process_subgraph_response_om_ok")
.await
.unwrap_err();
let processed_error = process_error(error);
assert_eq!(processed_error.status, StatusCode::OK);
assert_eq!(
processed_error.message,
Some("I have raised a 200".to_string())
);
}
#[tokio::test]
async fn it_cannot_process_om_subgraph_missing_message_and_body() {
if let Err(error) = call_rhai_function("process_subgraph_response_om_missing_message").await {
let processed_error = process_error(error);
assert_eq!(processed_error.status, StatusCode::BAD_REQUEST);
assert_eq!(
processed_error.message,
Some(
"rhai execution error: 'Runtime error: #{\"status\": 400} (line 262, position 5)'"
.to_string()
)
);
} else {
panic!("error processed incorrectly");
}
}
#[tokio::test]
async fn it_mentions_source_when_syntax_error_occurs() {
let err: Box<dyn std::error::Error> = crate::plugin::plugins()
.find(|factory| factory.name == "apollo.rhai")
.expect("Plugin not found")
.create_instance_without_schema(
&Value::from_str(r#"{"scripts":"tests/fixtures", "main":"syntax_errors.rhai"}"#)
.unwrap(),
)
.await
.err()
.unwrap();
assert!(err.to_string().contains("syntax_errors.rhai"));
}
#[test]
#[should_panic(
expected = "can use env: ErrorRuntime(\"could not expand variable: THIS_SHOULD_NOT_EXIST, environment variable not found\", none)"
)]
fn it_cannot_expand_missing_environment_variable() {
assert!(std::env::var("THIS_SHOULD_NOT_EXIST").is_err());
let engine = new_rhai_test_engine();
let _: String = engine
.eval(
r#"
env::get("THIS_SHOULD_NOT_EXIST")"#,
)
.expect("can use env");
}
#[test]
fn it_can_expand_environment_variable() {
let home = std::env::var("HOME").expect("can always read HOME");
let engine = new_rhai_test_engine();
let env_variable: String = engine
.eval(
r#"
env::get("HOME")"#,
)
.expect("can use env");
assert_eq!(home, env_variable);
}
#[test]
fn it_can_compare_method_strings() {
let mut engine = new_rhai_test_engine();
engine.register_fn(
"new_method",
|method: &str| -> Result<Method, Box<EvalAltResult>> {
Method::from_str(&method.to_uppercase()).map_err(|e| e.to_string().into())
},
);
let method: bool = engine
.eval(
r#"
let get = new_method("GEt").to_string();
get == "GET"
"#,
)
.expect("can compare properly");
assert!(method);
}
#[tokio::test]
async fn test_router_service_adds_timestamp_header() -> Result<(), BoxError> {
let mut mock_service = MockSupergraphService::new();
mock_service
.expect_call()
.times(1)
.returning(move |req: SupergraphRequest| {
Ok(SupergraphResponse::fake_builder()
.header("x-custom-header", "CUSTOM_VALUE")
.context(req.context)
.build()
.unwrap())
});
let dyn_plugin: Box<dyn DynPlugin> = crate::plugin::plugins()
.find(|factory| factory.name == "apollo.rhai")
.expect("Plugin not found")
.create_instance_without_schema(
&Value::from_str(r#"{"scripts":"tests/fixtures", "main":"remove_header.rhai"}"#)
.unwrap(),
)
.await
.unwrap();
let mut router_service = dyn_plugin.supergraph_service(BoxService::new(mock_service));
let context = Context::new();
context.insert("test", 5i64).unwrap();
let supergraph_req = SupergraphRequest::fake_builder()
.header("x-custom-header", "CUSTOM_VALUE")
.context(context)
.build()?;
let service_response = router_service.ready().await?.call(supergraph_req).await?;
assert_eq!(StatusCode::OK, service_response.response.status());
let headers = service_response.response.headers().clone();
assert!(headers.get("x-custom-header").is_none());
Ok(())
}
#[tokio::test]
async fn it_can_access_demand_control_context() -> Result<(), BoxError> {
let mut mock_service = MockSupergraphService::new();
mock_service
.expect_call()
.times(1)
.returning(move |req: SupergraphRequest| {
Ok(SupergraphResponse::fake_builder()
.context(req.context)
.build()
.unwrap())
});
let dyn_plugin: Box<dyn DynPlugin> = crate::plugin::plugins()
.find(|factory| factory.name == "apollo.rhai")
.expect("Plugin not found")
.create_instance_without_schema(
&Value::from_str(r#"{"scripts":"tests/fixtures", "main":"demand_control.rhai"}"#)
.unwrap(),
)
.await
.unwrap();
let mut router_service = dyn_plugin.supergraph_service(BoxService::new(mock_service));
let context = Context::new();
context.insert_estimated_cost(50.0).unwrap();
context.insert_actual_cost(35.0).unwrap();
context
.insert_cost_strategy("test_strategy".to_string())
.unwrap();
context.insert_cost_result("COST_OK".to_string()).unwrap();
let supergraph_req = SupergraphRequest::fake_builder().context(context).build()?;
let service_response = router_service.ready().await?.call(supergraph_req).await?;
assert_eq!(StatusCode::OK, service_response.response.status());
let headers = service_response.response.headers().clone();
let demand_control_header = headers
.get("demand-control-estimate")
.map(|h| h.to_str().unwrap());
assert_eq!(demand_control_header, Some("50.0"));
let demand_control_header = headers
.get("demand-control-actual")
.map(|h| h.to_str().unwrap());
assert_eq!(demand_control_header, Some("35.0"));
let demand_control_header = headers
.get("demand-control-strategy")
.map(|h| h.to_str().unwrap());
assert_eq!(demand_control_header, Some("test_strategy"));
let demand_control_header = headers
.get("demand-control-result")
.map(|h| h.to_str().unwrap());
assert_eq!(demand_control_header, Some("COST_OK"));
Ok(())
}