#![allow(clippy::unwrap_used, clippy::panic)] #![allow(clippy::print_stderr)]
use std::sync::Arc;
use fraiseql_core::{
db::{postgres::PostgresAdapter, traits::DatabaseAdapter},
runtime::{Executor, relay::encode_node_id},
schema::{CompiledSchema, FieldDefinition, FieldType, InterfaceDefinition},
};
use fraiseql_test_utils::schema_builder::{TestQueryBuilder, TestSchemaBuilder, TestTypeBuilder};
use serde_json::{Value, json};
const VIEW: &str = "v_selection_conformance_user";
const ALICE_UUID: &str = "aaaa0000-0000-0000-0000-000000000001";
const FIXTURE: &str = r#"
DROP TABLE IF EXISTS v_selection_conformance_user;
CREATE TABLE v_selection_conformance_user (pk_user bigint, data jsonb);
INSERT INTO v_selection_conformance_user (pk_user, data) VALUES
(1, '{"id":"aaaa0000-0000-0000-0000-000000000001","name":"Alice","pk_user":1,
"email":"alice@example.com","secret":"SECRET-VALUE",
"profile":{"tier":"gold"}}'::jsonb),
(2, '{"id":"bbbb0000-0000-0000-0000-000000000002","name":"Bob","pk_user":2,
"email":"bob@example.com","secret":"SECRET-VALUE",
"profile":{"tier":"silver"}}'::jsonb);
"#;
fn schema() -> CompiledSchema {
let user_type = TestTypeBuilder::new("User", VIEW)
.relay_node()
.with_implements(&["Node"])
.with_simple_field("id", FieldType::Uuid)
.with_simple_field("name", FieldType::String)
.with_simple_field("email", FieldType::String)
.with_simple_field("secret", FieldType::String)
.with_simple_field("profile", FieldType::Object("Profile".to_string()))
.build();
let profile_type = TestTypeBuilder::new("Profile", "v_selection_conformance_profile")
.with_simple_field("tier", FieldType::String)
.build();
let users_query = TestQueryBuilder::new("users", "User")
.returns_list(true)
.with_sql_source(VIEW)
.build();
let mut connection_query = TestQueryBuilder::new("usersConnection", "User")
.returns_list(true)
.with_sql_source(VIEW)
.relay_cursor_column("pk_user")
.build();
connection_query.auto_params.has_where = true;
connection_query.auto_params.has_order_by = true;
let mut schema = TestSchemaBuilder::new()
.with_type(user_type)
.with_type(profile_type)
.with_query(users_query)
.with_query(connection_query)
.build();
schema.interfaces.push(
InterfaceDefinition::new("Node").with_field(FieldDefinition::new("id", FieldType::Id)),
);
schema
}
async fn executor() -> Option<Executor> {
let pg = fraiseql_test_support::postgres().await?;
let adapter = PostgresAdapter::new(pg.url()).await.expect("connect to the bound PostgreSQL");
for stmt in FIXTURE.split(";\n") {
if stmt.trim().is_empty() {
continue;
}
adapter
.execute_raw_query(stmt)
.await
.expect("provision the conformance fixture");
}
Some(Executor::new_with_relay(schema(), Arc::new(adapter)))
}
fn node_keys(response: &Value) -> Vec<String> {
response["data"]["node"]
.as_object()
.unwrap_or_else(|| panic!("expected an object at data.node, got: {response}"))
.keys()
.cloned()
.collect()
}
fn user_keys(response: &Value) -> Vec<String> {
response["data"]["users"][0]
.as_object()
.unwrap_or_else(|| panic!("expected an object at data.users[0], got: {response}"))
.keys()
.cloned()
.collect()
}
fn edge_names(response: &Value) -> Vec<String> {
response["data"]["usersConnection"]["edges"]
.as_array()
.unwrap_or_else(|| panic!("expected edges at data.usersConnection, got: {response}"))
.iter()
.map(|e| {
e["node"]["name"]
.as_str()
.unwrap_or_else(|| panic!("edge node has no name: {response}"))
.to_string()
})
.collect()
}
struct Case {
name: &'static str,
query: &'static str,
expect: &'static [&'static str],
}
const NODE_MATRIX: &[Case] = &[
Case {
name: "node with a named spread",
query: "fragment F on User { name email } query { node(id: \"@ID@\") { id ...F } }",
expect: &["id", "name", "email"],
},
Case {
name: "node with a spread-only selection",
query: "fragment F on User { name } query { node(id: \"@ID@\") { ...F } }",
expect: &["name"],
},
Case {
name: "node with a skipped spread",
query: "fragment F on User { email } query { node(id: \"@ID@\") { id ...F @skip(if: \
true) } }",
expect: &["id"],
},
Case {
name: "node with an included spread",
query: "fragment F on User { email } query { node(id: \"@ID@\") { id ...F @include(if: \
true) } }",
expect: &["id", "email"],
},
Case {
name: "node with a skipped field",
query: "{ node(id: \"@ID@\") { id name @skip(if: true) } }",
expect: &["id"],
},
Case {
name: "node with an inline fragment",
query: "{ node(id: \"@ID@\") { id ... on User { email } } }",
expect: &["id", "email"],
},
Case {
name: "node with a skipped inline fragment",
query: "{ node(id: \"@ID@\") { id ... on User @skip(if: true) { email } } }",
expect: &["id"],
},
Case {
name: "node with a nested spread",
query: "fragment Inner on User { email } fragment Outer on User { name ...Inner } query \
{ node(id: \"@ID@\") { id ...Outer } }",
expect: &["id", "name", "email"],
},
Case {
name: "node with everything skipped",
query: "{ node(id: \"@ID@\") { id @skip(if: true) } }",
expect: &[],
},
];
#[tokio::test]
async fn node_query_returns_exactly_the_requested_fields() {
let Some(exec) = executor().await else {
eprintln!("SKIP: no PostgreSQL (set DATABASE_URL)");
return;
};
let node_id = encode_node_id("User", ALICE_UUID);
let mut failures = Vec::new();
for case in NODE_MATRIX {
let query = case.query.replace("@ID@", &node_id);
match exec.execute(&query, None).await {
Ok(response) => {
let keys = node_keys(&response);
if keys != case.expect {
failures.push(format!(
"{}: expected {:?}, got {keys:?}\n response: {response}",
case.name, case.expect
));
}
},
Err(e) => failures.push(format!("{}: execution failed: {e}", case.name)),
}
}
assert!(
failures.is_empty(),
"{} case(s) failed:\n {}",
failures.len(),
failures.join("\n ")
);
}
const QUERY_MATRIX: &[Case] = &[
Case {
name: "named spread",
query: "fragment F on User { name email } query { users { id ...F } }",
expect: &["id", "name", "email"],
},
Case {
name: "skipped spread",
query: "fragment F on User { name email } query { users { id ...F @skip(if: true) } }",
expect: &["id"],
},
Case {
name: "spread at its document position",
query: "fragment F on User { name } query { users { email ...F id } }",
expect: &["email", "name", "id"],
},
Case {
name: "nested spread, inner skipped",
query: "fragment Inner on User { email } fragment Outer on User { name ...Inner \
@skip(if: true) } query { users { id ...Outer } }",
expect: &["id", "name"],
},
Case {
name: "everything skipped",
query: "{ users { id @skip(if: true) } }",
expect: &[],
},
];
#[tokio::test]
async fn regular_query_returns_exactly_the_requested_fields() {
let Some(exec) = executor().await else {
eprintln!("SKIP: no PostgreSQL (set DATABASE_URL)");
return;
};
let mut failures = Vec::new();
for case in QUERY_MATRIX {
match exec.execute(case.query, None).await {
Ok(response) => {
let keys = user_keys(&response);
if keys != case.expect {
failures.push(format!(
"{}: expected {:?}, got {keys:?}\n response: {response}",
case.name, case.expect
));
}
},
Err(e) => failures.push(format!("{}: execution failed: {e}", case.name)),
}
}
assert!(
failures.is_empty(),
"{} case(s) failed:\n {}",
failures.len(),
failures.join("\n ")
);
}
#[tokio::test]
async fn an_unselected_field_never_reaches_the_response() {
let Some(exec) = executor().await else {
eprintln!("SKIP: no PostgreSQL (set DATABASE_URL)");
return;
};
let node_id = encode_node_id("User", ALICE_UUID);
for query in [
format!("fragment F on User {{ name }} query {{ node(id: \"{node_id}\") {{ ...F }} }}"),
format!("{{ node(id: \"{node_id}\") {{ id @skip(if: true) }} }}"),
format!("{{ node(id: \"{node_id}\") {{ id }} }}"),
"fragment F on User { name } query { users { ...F } }".to_string(),
] {
let response = exec.execute(&query, None).await.expect("query must run");
let rendered = response.to_string();
assert!(
!rendered.contains("SECRET-VALUE"),
"unselected field served for `{query}`: {rendered}"
);
}
let response = exec
.execute(&format!("{{ node(id: \"{node_id}\") {{ secret }} }}"), None)
.await
.expect("query must run");
assert_eq!(response["data"]["node"]["secret"], json!("SECRET-VALUE"), "{response}");
}
#[tokio::test]
async fn multi_root_query_carries_fragments_and_directives() {
let Some(exec) = executor().await else {
eprintln!("SKIP: no PostgreSQL (set DATABASE_URL)");
return;
};
let response = exec
.execute(
"fragment F on User { name } query { a: users { id ...F } b: users { id email \
@skip(if: true) } }",
None,
)
.await
.expect("multi-root query with a fragment must run");
let a_keys: Vec<String> = response["data"]["a"][0]
.as_object()
.unwrap_or_else(|| panic!("{response}"))
.keys()
.cloned()
.collect();
let b_keys: Vec<String> = response["data"]["b"][0]
.as_object()
.unwrap_or_else(|| panic!("{response}"))
.keys()
.cloned()
.collect();
assert_eq!(a_keys, vec!["id", "name"], "{response}");
assert_eq!(b_keys, vec!["id"], "{response}");
}
#[tokio::test]
async fn an_undeclared_field_never_reaches_the_database() {
let Some(exec) = executor().await else {
eprintln!("SKIP: no PostgreSQL (set DATABASE_URL)");
return;
};
let err = exec
.execute("{ users { phantom_field } }", None)
.await
.expect_err("a selection on an undeclared field is an invalid document (§ 5.3.1)");
let msg = err.to_string();
assert!(
msg.contains("phantom_field") && msg.contains("User"),
"the error names the field and the type it is not on, got: {msg}"
);
for query in [
"{ users { id profile { nope } } }",
"fragment F on User { emial } query { users { id ...F } }",
] {
let err = exec.execute(query, None).await.expect_err(&format!("invalid: {query}"));
assert!(
matches!(err, fraiseql_core::error::FraiseQLError::Validation { .. }),
"expected a validation error for `{query}`, got: {err:?}"
);
}
let ok = exec
.execute("{ users { id name email } }", None)
.await
.expect("the declared shape must still run");
assert_eq!(ok["data"]["users"][0]["name"], json!("Alice"), "{ok}");
}
#[tokio::test]
async fn nested_typename_survives_the_sql_projection() {
let Some(exec) = executor().await else {
eprintln!("SKIP: no PostgreSQL (set DATABASE_URL)");
return;
};
let response = exec
.execute("{ users { id profile { __typename tier } } }", None)
.await
.expect("nested-typename query must run");
let profile = response["data"]["users"][0]["profile"]
.as_object()
.unwrap_or_else(|| panic!("expected a profile object: {response}"));
assert_eq!(
profile.get("__typename").and_then(Value::as_str),
Some("Profile"),
"`__typename` is `String!` — a requested nested one is neither null nor absent: {response}"
);
assert_eq!(
profile.get("tier").and_then(Value::as_str),
Some("gold"),
"the sibling field must still be projected: {response}"
);
let keys: Vec<&str> = profile.keys().map(String::as_str).collect();
assert_eq!(
keys,
vec!["__typename", "tier"],
"the response's fields follow the query's order: {response}"
);
let plain = exec
.execute("{ users { profile { tier } } }", None)
.await
.expect("control query must run");
assert!(
!plain["data"]["users"][0]["profile"]
.as_object()
.unwrap_or_else(|| panic!("{plain}"))
.contains_key("__typename"),
"an unrequested `__typename` must be absent, not null: {plain}"
);
}
#[tokio::test]
async fn relay_inline_arguments_narrow_the_page() {
let Some(exec) = executor().await else {
eprintln!("SKIP: no PostgreSQL (set DATABASE_URL)");
return;
};
let all = exec
.execute("{ usersConnection { edges { node { name } } } }", None)
.await
.expect("unfiltered relay query must run");
assert_eq!(
edge_names(&all),
vec!["Alice", "Bob"],
"the unfiltered connection is the baseline the filtered cases narrow: {all}"
);
let inline = exec
.execute(
r#"{ usersConnection(where: { name: { eq: "Alice" } }) { edges { node { name } } } }"#,
None,
)
.await
.expect("inline-filtered relay query must run");
assert_eq!(
edge_names(&inline),
vec!["Alice"],
"an inline `where:` must narrow the page; serving both rows is the whole of #904 — \
the response is a superset of what was asked for, and nothing says so: {inline}"
);
let via_variable = exec
.execute(
"query($w: UserWhereInput) { usersConnection(where: $w) { edges { node { name } } } }",
Some(&json!({"w": {"name": {"eq": "Alice"}}})),
)
.await
.expect("variable-filtered relay query must run");
assert_eq!(
edge_names(&via_variable),
edge_names(&inline),
"the inline and variable forms of `where:` must return the same rows: {via_variable}"
);
let first_one = exec
.execute("{ usersConnection(first: 1) { edges { node { name } } } }", None)
.await
.expect("inline `first:` relay query must run");
assert_eq!(
edge_names(&first_one),
vec!["Alice"],
"an inline `first: 1` must bound the page to one edge: {first_one}"
);
}