use std::collections::HashMap;
use std::sync::Arc;
use tower::BoxError;
use tower::ServiceExt;
use crate::configuration::Configuration;
use crate::plugin::test::canned;
use crate::plugin::test::MockSubgraph;
use crate::plugin::DynPlugin;
use crate::plugin::Plugin;
use crate::plugin::PluginInit;
use crate::router_factory::SupergraphServiceConfigurator;
use crate::router_factory::YamlSupergraphServiceFactory;
use crate::services::execution;
use crate::services::subgraph;
use crate::services::supergraph;
use crate::services::RouterCreator;
use crate::Schema;
#[cfg(test)]
pub(crate) mod http_client;
pub struct TestHarness<'a> {
schema: Option<&'a str>,
configuration: Option<Arc<Configuration>>,
extra_plugins: Vec<(String, Box<dyn DynPlugin>)>,
subgraph_network_requests: bool,
}
impl<'a> TestHarness<'a> {
pub fn builder() -> Self {
Self {
schema: None,
configuration: None,
extra_plugins: Vec::new(),
subgraph_network_requests: false,
}
}
pub fn schema(mut self, schema: &'a str) -> Self {
assert!(self.schema.is_none(), "schema was specified twice");
self.schema = Some(schema);
self
}
pub fn configuration(mut self, configuration: Arc<Configuration>) -> Self {
assert!(
self.configuration.is_none(),
"configuration was specified twice"
);
self.configuration = Some(configuration);
self
}
pub fn configuration_json(
self,
configuration: serde_json::Value,
) -> Result<Self, serde_json::Error> {
Ok(self.configuration(serde_json::from_value(configuration)?))
}
pub fn extra_plugin<P: Plugin>(mut self, plugin: P) -> Self {
let type_id = std::any::TypeId::of::<P>();
let name = match crate::plugin::plugins()
.iter()
.find(|(_name, factory)| factory.type_id == type_id)
{
Some((name, _factory)) => name.clone(),
None => format!(
"extra_plugins.{}.{}",
self.extra_plugins.len(),
std::any::type_name::<P>(),
),
};
self.extra_plugins.push((name, Box::new(plugin)));
self
}
pub fn supergraph_hook(
self,
callback: impl Fn(supergraph::BoxService) -> supergraph::BoxService + Send + Sync + 'static,
) -> Self {
self.extra_plugin(SupergraphServicePlugin(callback))
}
pub fn execution_hook(
self,
callback: impl Fn(execution::BoxService) -> execution::BoxService + Send + Sync + 'static,
) -> Self {
self.extra_plugin(ExecutionServicePlugin(callback))
}
pub fn subgraph_hook(
self,
callback: impl Fn(&str, subgraph::BoxService) -> subgraph::BoxService + Send + Sync + 'static,
) -> Self {
self.extra_plugin(SubgraphServicePlugin(callback))
}
pub fn with_subgraph_network_requests(mut self) -> Self {
self.subgraph_network_requests = true;
self
}
async fn build_common(self) -> Result<(Arc<Configuration>, RouterCreator), BoxError> {
let builder = if self.schema.is_none() {
self.subgraph_hook(|subgraph_name, default| match subgraph_name {
"products" => canned::products_subgraph().boxed(),
"accounts" => canned::accounts_subgraph().boxed(),
"reviews" => canned::reviews_subgraph().boxed(),
_ => default,
})
} else {
self
};
let builder = if builder.subgraph_network_requests {
builder
} else {
builder.subgraph_hook(|_name, _default| {
tower::service_fn(|request: subgraph::Request| {
let empty_response = subgraph::Response::builder()
.extensions(crate::json_ext::Object::new())
.context(request.context)
.build();
std::future::ready(Ok(empty_response))
})
.boxed()
})
};
let config = builder.configuration.unwrap_or_default();
let canned_schema = include_str!("../testing_schema.graphql");
let schema = builder.schema.unwrap_or(canned_schema);
let schema = Arc::new(Schema::parse(schema, &config)?);
let router_creator = YamlSupergraphServiceFactory
.create(config.clone(), schema, None, Some(builder.extra_plugins))
.await?;
Ok((config, router_creator))
}
pub async fn build(self) -> Result<supergraph::BoxCloneService, BoxError> {
let (_config, router_creator) = self.build_common().await?;
Ok(tower::service_fn(move |request| {
let service = router_creator.make();
async move { service.oneshot(request).await }
})
.boxed_clone())
}
#[cfg(test)]
pub(crate) async fn build_http_service(self) -> Result<HttpService, BoxError> {
use crate::axum_http_server_factory::make_axum_router;
use crate::axum_http_server_factory::ListenAddrAndRouter;
use crate::router_factory::SupergraphServiceFactory;
let (config, router_creator) = self.build_common().await?;
let web_endpoints = router_creator.web_endpoints();
let routers = make_axum_router(router_creator, &config, web_endpoints)?;
let ListenAddrAndRouter(_listener, router) = routers.main;
Ok(router.boxed())
}
}
#[cfg(test)]
pub(crate) type HttpService = tower::util::BoxService<
http::Request<hyper::Body>,
http::Response<axum::body::BoxBody>,
std::convert::Infallible,
>;
struct SupergraphServicePlugin<F>(F);
struct ExecutionServicePlugin<F>(F);
struct SubgraphServicePlugin<F>(F);
#[async_trait::async_trait]
impl<F> Plugin for SupergraphServicePlugin<F>
where
F: 'static + Send + Sync + Fn(supergraph::BoxService) -> supergraph::BoxService,
{
type Config = ();
async fn new(_: PluginInit<Self::Config>) -> Result<Self, BoxError> {
unreachable!()
}
fn supergraph_service(&self, service: supergraph::BoxService) -> supergraph::BoxService {
(self.0)(service)
}
}
#[async_trait::async_trait]
impl<F> Plugin for ExecutionServicePlugin<F>
where
F: 'static + Send + Sync + Fn(execution::BoxService) -> execution::BoxService,
{
type Config = ();
async fn new(_: PluginInit<Self::Config>) -> Result<Self, BoxError> {
unreachable!()
}
fn execution_service(&self, service: execution::BoxService) -> execution::BoxService {
(self.0)(service)
}
}
#[async_trait::async_trait]
impl<F> Plugin for SubgraphServicePlugin<F>
where
F: 'static + Send + Sync + Fn(&str, subgraph::BoxService) -> subgraph::BoxService,
{
type Config = ();
async fn new(_: PluginInit<Self::Config>) -> Result<Self, BoxError> {
unreachable!()
}
fn subgraph_service(
&self,
subgraph_name: &str,
service: subgraph::BoxService,
) -> subgraph::BoxService {
(self.0)(subgraph_name, service)
}
}
#[derive(Default)]
pub struct MockedSubgraphs(pub(crate) HashMap<&'static str, MockSubgraph>);
impl MockedSubgraphs {
pub fn insert(&mut self, name: &'static str, subgraph: MockSubgraph) {
self.0.insert(name, subgraph);
}
}
#[async_trait::async_trait]
impl Plugin for MockedSubgraphs {
type Config = ();
async fn new(_: PluginInit<Self::Config>) -> Result<Self, BoxError> {
unreachable!()
}
fn subgraph_service(
&self,
subgraph_name: &str,
default: subgraph::BoxService,
) -> subgraph::BoxService {
self.0
.get(subgraph_name)
.map(|service| service.clone().boxed())
.unwrap_or(default)
}
}