#![allow(clippy::unwrap_used)]
mod explain_tests {
#![allow(clippy::unwrap_used)] use std::sync::Arc;
use async_trait::async_trait;
use serde_json::json;
use crate::{
db::{
DatabaseType, PoolMetrics, WhereClause,
types::{JsonbValue, OrderByClause},
},
error::{FraiseQLError, Result},
runtime::{Executor, executor::support::explain::*},
schema::{CompiledSchema, MutationDefinition, QueryDefinition},
};
struct MockAdapter;
#[async_trait]
impl crate::db::traits::DatabaseAdapter for MockAdapter {
async fn execute_where_query(
&self,
_view: &str,
_where_clause: Option<&WhereClause>,
_limit: Option<u32>,
_offset: Option<u32>,
_order_by: Option<&[OrderByClause]>,
) -> Result<Vec<JsonbValue>> {
Ok(vec![])
}
async fn execute_with_projection(
&self,
_view: &str,
_projection: Option<&crate::schema::SqlProjectionHint>,
_where_clause: Option<&WhereClause>,
_limit: Option<u32>,
_offset: Option<u32>,
_order_by: Option<&[OrderByClause]>,
) -> Result<Vec<JsonbValue>> {
Ok(vec![])
}
fn database_type(&self) -> DatabaseType {
DatabaseType::SQLite
}
async fn health_check(&self) -> Result<()> {
Ok(())
}
fn pool_metrics(&self) -> PoolMetrics {
PoolMetrics {
total_connections: 1,
idle_connections: 1,
active_connections: 0,
waiting_requests: 0,
}
}
async fn execute_raw_query(
&self,
_sql: &str,
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
Ok(vec![])
}
async fn execute_parameterized_aggregate(
&self,
_sql: &str,
_params: &[serde_json::Value],
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
Ok(vec![])
}
}
fn make_schema_with_query(name: &str, sql_source: &str) -> CompiledSchema {
let mut schema = CompiledSchema::default();
let mut qd = QueryDefinition::new(name, "SomeType");
qd.sql_source = Some(sql_source.to_string());
schema.queries.push(qd);
schema
}
fn make_schema_with_mutation(name: &str) -> CompiledSchema {
let mut schema = CompiledSchema::default();
let mut md = MutationDefinition::new(name, "MutationResponse");
md.sql_source = Some(format!("fn_{name}"));
schema.mutations.push(md);
schema
}
#[tokio::test]
async fn test_explain_unknown_query_returns_error() {
let schema = make_schema_with_query("users", "v_user");
let executor = Executor::new(schema, Arc::new(MockAdapter));
let err = executor.explain("nonexistent", None, None, None).await.unwrap_err();
assert!(
matches!(&err, FraiseQLError::Validation { message, .. } if message.contains("nonexistent")),
"expected Validation error mentioning the query name, got: {err:?}"
);
}
#[tokio::test]
async fn test_explain_mutation_returns_error() {
let schema = make_schema_with_mutation("createUser");
let executor = Executor::new(schema, Arc::new(MockAdapter));
let err = executor.explain("createUser", None, None, None).await.unwrap_err();
assert!(
matches!(&err, FraiseQLError::Validation { message, .. } if message.contains("mutation")),
"expected Validation error mentioning mutation, got: {err:?}"
);
}
#[tokio::test]
async fn test_explain_unsupported_adapter_returns_error() {
let schema = make_schema_with_query("users", "v_user");
let executor = Executor::new(schema, Arc::new(MockAdapter));
let err = executor
.explain("users", Some(&json!({"status": "active"})), Some(10), None)
.await
.unwrap_err();
assert!(
matches!(&err, FraiseQLError::Unsupported { .. }),
"expected Unsupported error from mock adapter, got: {err:?}"
);
}
#[test]
fn test_build_display_sql_no_clause() {
let sql = build_display_sql("v_user", None, None, None);
assert_eq!(sql, "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT data FROM \"v_user\"");
}
#[test]
fn test_build_display_sql_with_limit_offset() {
let vars = json!({"status": "active"});
let sql = build_display_sql("v_user", Some(&vars), Some(10), Some(20));
assert!(sql.contains("LIMIT $2"), "should contain LIMIT $2, got: {sql}");
assert!(sql.contains("OFFSET $3"), "should contain OFFSET $3, got: {sql}");
}
}
mod pipeline_tests {
#![allow(clippy::unwrap_used)] use std::sync::Arc;
use async_trait::async_trait;
use crate::{
db::{
WhereClause,
types::{DatabaseType, JsonbValue, OrderByClause, PoolMetrics},
},
error::FraiseQLError,
graphql::{ParsedQuery, parse_query},
runtime::{Executor, RuntimeConfig, executor::support::pipeline::*},
schema::{CompiledSchema, QueryDefinition, SqlProjectionHint},
security::{QueryValidatorConfig, SecurityContext},
};
fn parsed(query: &str) -> ParsedQuery {
parse_query(query).expect("valid query")
}
fn make_schema_with_queries(names: &[(&str, &str)]) -> CompiledSchema {
let mut schema = CompiledSchema::default();
for (name, sql_source) in names {
let mut qd = QueryDefinition::new(*name, "SomeType");
qd.sql_source = Some((*sql_source).to_string());
qd.returns_list = true;
schema.queries.push(qd);
}
schema
}
struct MockAdapter;
#[async_trait]
impl crate::db::traits::DatabaseAdapter for MockAdapter {
async fn execute_where_query(
&self,
_view: &str,
_where_clause: Option<&WhereClause>,
_limit: Option<u32>,
_offset: Option<u32>,
_order_by: Option<&[OrderByClause]>,
) -> crate::error::Result<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]>,
) -> crate::error::Result<Vec<JsonbValue>> {
Ok(vec![JsonbValue::new(serde_json::json!({"id": 1}))])
}
fn database_type(&self) -> DatabaseType {
DatabaseType::SQLite
}
async fn health_check(&self) -> crate::error::Result<()> {
Ok(())
}
fn pool_metrics(&self) -> PoolMetrics {
PoolMetrics {
total_connections: 1,
idle_connections: 1,
active_connections: 0,
waiting_requests: 0,
}
}
async fn execute_raw_query(
&self,
_sql: &str,
) -> crate::error::Result<Vec<std::collections::HashMap<String, serde_json::Value>>>
{
Ok(vec![])
}
async fn execute_parameterized_aggregate(
&self,
_sql: &str,
_params: &[serde_json::Value],
) -> crate::error::Result<Vec<std::collections::HashMap<String, serde_json::Value>>>
{
Ok(vec![])
}
}
fn make_executor(names: &[(&str, &str)]) -> Executor<MockAdapter> {
let schema = make_schema_with_queries(names);
Executor::new(schema, Arc::new(MockAdapter))
}
#[test]
fn test_is_multi_root_single() {
assert!(!is_multi_root(&parsed("{ users { id } }")));
}
#[test]
fn test_is_multi_root_two_roots() {
assert!(is_multi_root(&parsed("{ users { id } posts { id } }")));
}
#[test]
fn test_is_multi_root_three_roots() {
assert!(is_multi_root(&parsed("{ users { id } posts { id } orders { id } }")));
}
#[test]
fn test_extract_root_field_names_single() {
let p = parsed("{ users { id } }");
let names: Vec<&str> = extract_root_field_names(&p).collect();
assert_eq!(names, vec!["users"], "single root selection should yield one name");
}
#[test]
fn test_extract_root_field_names_two() {
let p = parsed("{ users { id } posts { id } }");
let names: Vec<&str> = extract_root_field_names(&p).collect();
assert_eq!(
names,
vec!["users", "posts"],
"two root selections should yield both names in order"
);
}
#[test]
fn test_serializer_simple_field() {
let p = parsed("{ users { id name } }");
let field = &p.selections[0];
let q = field_selection_to_query(field);
assert!(q.contains("users"), "missing field name: {q}");
assert!(q.contains("id"), "missing subfield: {q}");
assert!(q.contains("name"), "missing subfield: {q}");
}
#[test]
fn test_serializer_scalar_arg() {
let p = parsed("{ users(limit: 10) { id } }");
let field = &p.selections[0];
let q = field_selection_to_query(field);
assert!(q.contains("limit"), "missing arg: {q}");
assert!(q.contains("10"), "missing value: {q}");
}
#[test]
fn test_serializer_roundtrip_is_parseable() {
let original = "{ users { id name } }";
let p = parsed(original);
let synthetic = field_selection_to_query(&p.selections[0]);
parse_query(&synthetic).expect("synthetic query must be valid GraphQL");
}
#[tokio::test]
async fn test_execute_parallel_returns_all_fields() {
let exec = make_executor(&[("users", "v_users"), ("posts", "v_posts")]);
let p = parsed("{ users { id } posts { id } }");
let result = exec.execute_parallel(&p, None, None).await.unwrap();
assert_eq!(result.fields.len(), 2);
assert!(result.fields.iter().any(|f| f.field_name == "users"));
assert!(result.fields.iter().any(|f| f.field_name == "posts"));
assert!(result.parallel);
}
#[tokio::test]
async fn test_execute_parallel_merges_data_correctly() {
let exec = make_executor(&[("users", "v_users"), ("posts", "v_posts")]);
let p = parsed("{ users { id } posts { id } }");
let result = exec.execute_parallel(&p, None, None).await.unwrap();
let merged = result.merge_into_data_map();
assert!(merged.contains_key("users"), "missing users key");
assert!(merged.contains_key("posts"), "missing posts key");
}
#[tokio::test]
async fn test_single_root_unaffected() {
let exec = make_executor(&[("users", "v_users")]);
let val = exec.execute("{ users { id } }", None).await.unwrap();
assert!(val["data"]["users"].is_array());
}
#[tokio::test]
async fn test_multi_root_counter_increments() {
let before = multi_root_queries_total();
let exec = make_executor(&[("users", "v_users"), ("posts", "v_posts")]);
let p = parsed("{ users { id } posts { id } }");
exec.execute_parallel(&p, None, None).await.unwrap();
assert!(multi_root_queries_total() > before);
}
fn auth_ctx() -> SecurityContext {
SecurityContext {
user_id: "user-1".into(),
roles: vec![],
tenant_id: None,
scopes: vec![],
attributes: std::collections::HashMap::default(),
request_id: "req-1".to_string(),
ip_address: None,
expires_at: chrono::Utc::now() + chrono::Duration::hours(1),
authenticated_at: chrono::Utc::now(),
issuer: None,
audience: None,
email: None,
display_name: None,
}
}
#[tokio::test]
async fn authenticated_multi_root_returns_all_roots() {
let exec = make_executor(&[("users", "v_users"), ("posts", "v_posts")]);
let ctx = auth_ctx();
let val = exec
.execute_with_security("{ users { id } posts { id } }", None, &ctx)
.await
.unwrap();
assert!(val["data"].get("users").is_some(), "missing users root");
assert!(
val["data"].get("posts").is_some(),
"missing posts root — authenticated multi-root was silently dropped (H19)"
);
}
#[tokio::test]
async fn gate1_validation_fires_on_authenticated_path() {
let schema = make_schema_with_queries(&[("users", "v_users")]);
let config = RuntimeConfig {
query_validation: Some(QueryValidatorConfig {
max_depth: 50,
max_complexity: 10_000,
max_size_bytes: 5, max_aliases: 1_000,
}),
..RuntimeConfig::default()
};
let exec = Executor::with_config(schema, Arc::new(MockAdapter), config);
let ctx = auth_ctx();
let result = exec.execute_with_security("{ users { id } }", None, &ctx).await;
assert!(
result.is_err(),
"GATE-1 must reject an oversize query on the authenticated path (L-gate1-skip)"
);
assert!(matches!(result.unwrap_err(), FraiseQLError::Validation { .. }));
}
}