use std::collections::HashMap;
use std::sync::Arc;
use tower::BoxError;
use tower::ServiceBuilder;
use tower::ServiceExt;
use tower_http::trace::MakeSpan;
use tracing_futures::Instrument;
use crate::axum_factory::utils::PropagatingMakeSpan;
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::YamlRouterFactory;
use crate::services::execution;
use crate::services::router;
use crate::services::router_service::RouterCreator;
use crate::services::subgraph;
use crate::services::supergraph;
use crate::services::SupergraphCreator;
use crate::spec::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().find(|factory| factory.type_id == type_id) {
Some(factory) => 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 router_hook(
self,
callback: impl Fn(router::BoxService) -> router::BoxService + Send + Sync + 'static,
) -> Self {
self.extra_plugin(RouterServicePlugin(callback))
}
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>, SupergraphCreator), 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 supergraph_creator = YamlRouterFactory
.create_supergraph(config.clone(), schema, None, Some(builder.extra_plugins))
.await?;
Ok((config, supergraph_creator))
}
#[deprecated = "use build_supergraph instead"]
pub async fn build(self) -> Result<supergraph::BoxCloneService, BoxError> {
self.build_supergraph().await
}
pub async fn build_supergraph(self) -> Result<supergraph::BoxCloneService, BoxError> {
let (_config, supergraph_creator) = self.build_common().await?;
Ok(tower::service_fn(move |request| {
let router = supergraph_creator.make();
async move { router.oneshot(request).await }
})
.boxed_clone())
}
pub async fn build_router(self) -> Result<router::BoxCloneService, BoxError> {
let (config, supergraph_creator) = self.build_common().await?;
let router_creator = RouterCreator::new(Arc::new(supergraph_creator), &config).await;
Ok(tower::service_fn(move |request: router::Request| {
let router = ServiceBuilder::new().service(router_creator.make()).boxed();
let span = PropagatingMakeSpan::default().make_span(&request.router_request);
async move { router.oneshot(request).await }.instrument(span)
})
.boxed_clone())
}
#[cfg(test)]
pub(crate) async fn build_http_service(self) -> Result<HttpService, BoxError> {
use crate::axum_factory::tests::make_axum_router;
use crate::axum_factory::ListenAddrAndRouter;
use crate::router_factory::RouterFactory;
let (config, supergraph_creator) = self.build_common().await?;
let router_creator = RouterCreator::new(Arc::new(supergraph_creator), &config).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 RouterServicePlugin<F>(F);
struct SupergraphServicePlugin<F>(F);
struct ExecutionServicePlugin<F>(F);
struct SubgraphServicePlugin<F>(F);
#[async_trait::async_trait]
impl<F> Plugin for RouterServicePlugin<F>
where
F: 'static + Send + Sync + Fn(router::BoxService) -> router::BoxService,
{
type Config = ();
async fn new(_: PluginInit<Self::Config>) -> Result<Self, BoxError> {
unreachable!()
}
fn router_service(&self, service: router::BoxService) -> router::BoxService {
(self.0)(service)
}
}
#[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)
}
}