#![allow(clippy::unwrap_used)] #![allow(clippy::panic)] #![allow(clippy::missing_panics_doc)] #![allow(missing_docs)]
#![cfg(feature = "functions-runtime")]
use std::{collections::HashMap, sync::Arc};
use async_trait::async_trait;
use fraiseql_core::{
db::{
DatabaseAdapter, DatabaseType, WhereClause,
types::{JsonbValue, OrderByClause, PoolMetrics},
},
error::Result as FraiseQLResult,
schema::{CompiledSchema, SqlProjectionHint},
};
use fraiseql_server::{Server, schema::loader::FunctionsConfig, server_config::ServerConfig};
#[derive(Debug, Clone)]
struct NoopAdapter;
#[async_trait]
impl DatabaseAdapter for NoopAdapter {
async fn execute_where_query(
&self,
_view: &str,
_where_clause: Option<&WhereClause>,
_limit: Option<u32>,
_offset: Option<u32>,
_order_by: Option<&[OrderByClause]>,
) -> FraiseQLResult<Vec<JsonbValue>> {
Ok(vec![])
}
async fn execute_with_projection(
&self,
_view: &str,
_projection: Option<&SqlProjectionHint>,
_where_clause: Option<&WhereClause>,
_limit: Option<u32>,
_offset: Option<u32>,
_order_by: Option<&[OrderByClause]>,
) -> FraiseQLResult<Vec<JsonbValue>> {
Ok(vec![])
}
fn database_type(&self) -> DatabaseType {
DatabaseType::PostgreSQL
}
async fn health_check(&self) -> FraiseQLResult<()> {
Ok(())
}
fn pool_metrics(&self) -> PoolMetrics {
PoolMetrics::default()
}
async fn execute_raw_query(
&self,
_sql: &str,
) -> FraiseQLResult<Vec<HashMap<String, serde_json::Value>>> {
Ok(vec![])
}
async fn execute_parameterized_aggregate(
&self,
_sql: &str,
_params: &[serde_json::Value],
) -> FraiseQLResult<Vec<HashMap<String, serde_json::Value>>> {
Ok(vec![])
}
}
#[async_trait::async_trait]
impl fraiseql_core::db::traits::Writer for NoopAdapter {
async fn execute_write(
&self,
request: &fraiseql_core::db::traits::WriteRequest<'_>,
gate: fraiseql_core::db::traits::MutationRowGate<'_>,
) -> std::result::Result<
Vec<std::collections::HashMap<String, serde_json::Value>>,
fraiseql_core::error::FraiseQLError,
> {
let _ = (request, gate);
Err(fraiseql_core::error::FraiseQLError::Unsupported {
message: "this test double does not write".to_string(),
})
}
}
fn schema_with_a_function_backed_query() -> CompiledSchema {
use fraiseql_core::schema::{FieldDefinition, FieldType, QueryDefinition, TypeDefinition};
let mut schema = CompiledSchema::new();
schema.types.push(TypeDefinition {
fields: vec![FieldDefinition::new("id", FieldType::Id)],
..TypeDefinition::new("Quote", "")
});
let mut query = QueryDefinition::new("quotePreview", "Quote").with_function("preview_quote");
query.nullable = true;
schema.queries.push(query);
schema.build_indexes();
schema
}
fn functions_with_an_invalid_module(dir: &std::path::Path) -> FunctionsConfig {
std::fs::write(dir.join("preview_quote.wasm"), b"not a wasm module").expect("write module");
serde_json::from_value(serde_json::json!({
"module_dir": dir.to_str().expect("utf-8 temp dir"),
"definitions": [
{ "name": "preview_quote", "trigger": "request:query", "runtime": "Wasm" }
]
}))
.expect("FunctionsConfig fixture")
}
fn server_config() -> ServerConfig {
ServerConfig {
schema_path: "/nonexistent/schema.compiled.json".into(),
cors_enabled: false,
cache_enabled: false,
..ServerConfig::default()
}
}
async fn error_for_query(functions: Option<FunctionsConfig>) -> String {
let server = Server::new(
server_config(),
schema_with_a_function_backed_query(),
Arc::new(NoopAdapter),
None,
)
.await
.expect("Server::new")
.with_functions_config(functions);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("addr");
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
let handle = tokio::spawn(async move {
let _ = server
.serve_on_listener(listener, async {
let _ = rx.await;
})
.await;
});
let client = reqwest::Client::new();
let response = client
.post(format!("http://{addr}/graphql"))
.json(&serde_json::json!({"query": "{ quotePreview { id } }"}))
.send()
.await
.expect("request")
.text()
.await
.expect("body");
let _ = tx.send(());
let _ = handle.await;
response
}
#[tokio::test]
async fn a_declared_functions_section_installs_the_query_resolver() {
let dir = tempfile::tempdir().expect("temp dir");
let body = Box::pin(error_for_query(Some(functions_with_an_invalid_module(dir.path())))).await;
assert!(
!body.contains("no function resolver wired"),
"the resolver must be installed when a functions section travels with the server; \
got: {body}"
);
assert!(
body.contains("quotePreview") || body.contains("preview_quote"),
"and the failure must still name the field or its function; got: {body}"
);
}
#[tokio::test]
async fn without_a_functions_section_the_field_is_refused_by_name() {
let body = Box::pin(error_for_query(None)).await;
assert!(
body.contains("no function resolver wired"),
"a function-backed field on a server with no functions must be refused by name; \
got: {body}"
);
assert!(
body.contains("quotePreview") && body.contains("preview_quote"),
"and the refusal must name the field and its function; got: {body}"
);
}