#![allow(clippy::unwrap_used)] #![allow(clippy::panic)]
use fraiseql_cli::schema::{ConvertOptions, SchemaConverter, intermediate::IntermediateSchema};
use serde_json::{Value, json};
fn corpus(queries: Value, functions: Value) -> Value {
let mut schema = json!({
"types": [{
"name": "Quote",
"sql_source": "v_quote",
"is_input": false,
"fields": [{"name": "id", "type": "ID", "nullable": false},
{"name": "total", "type": "Int", "nullable": false}]
}],
"mutations": [],
});
schema["queries"] = queries;
schema["functions"] = functions;
schema
}
fn preview_quote() -> Value {
json!([{"name": "preview_quote", "trigger": "request:query", "runtime": "Deno"}])
}
fn function_backed_query() -> Value {
json!([{
"name": "quotePreview",
"return_type": "Quote",
"returns_list": false,
"nullable": true,
"function": "preview_quote",
"arguments": [{"name": "sku", "type": "String", "nullable": false}]
}])
}
fn refusal(corpus: Value) -> String {
let intermediate: IntermediateSchema =
serde_json::from_value(corpus).expect("the corpus must deserialize");
match SchemaConverter::convert_artifact(intermediate, &ConvertOptions::default()) {
Ok(_) => panic!("this declaration must not compile"),
Err(error) => format!("{error:#}"),
}
}
fn accepts(corpus: Value) -> fraiseql_cli::schema::CompiledArtifact {
let intermediate: IntermediateSchema =
serde_json::from_value(corpus).expect("the corpus must deserialize");
SchemaConverter::convert_artifact(intermediate, &ConvertOptions::default())
.expect("this declaration must compile")
}
fn refused_for_the_binding(corpus: Value) -> String {
let message = refusal(corpus);
assert!(
message.contains("function = 'preview_quote'"),
"this must be refused by the function-backed-query rule, which names the binding — \
another check answering first would be a test passing for the wrong reason; \
got: {message}"
);
message
}
fn query_with(key: &str, value: Value) -> Value {
let mut queries = function_backed_query();
queries[0][key] = value;
queries
}
#[test]
fn a_function_backed_query_compiles_and_carries_only_the_binding() {
let artifact = accepts(corpus(function_backed_query(), preview_quote()));
let query = artifact
.schema
.queries
.iter()
.find(|q| q.name == "quotePreview")
.expect("the query must survive the compile");
assert_eq!(query.function.as_deref(), Some("preview_quote"));
assert_eq!(query.sql_source, None, "no relation may be invented for it");
assert_eq!(query.pagination_order, None, "it pages over nothing");
assert!(!query.auto_params.has_where, "auto-params lower into SQL it does not emit");
assert!(!query.auto_params.has_limit);
assert_eq!(
query.arguments.len(),
1,
"its declared arguments are typed from the schema like any other query's"
);
assert_eq!(query.arguments[0].name, "sku");
}
#[test]
fn a_sql_backed_query_emits_no_function_key() {
let queries = json!([{
"name": "quotes", "return_type": "Quote", "returns_list": true,
"nullable": false, "sql_source": "v_quote", "arguments": []
}]);
let artifact = accepts(corpus(queries, json!([])));
let value = serde_json::to_value(&artifact.schema.queries[0]).unwrap();
assert!(
value.get("function").is_none(),
"an absent binding must not be spelled out: {value}"
);
}
#[test]
fn project_wide_query_defaults_do_not_reach_a_function_backed_query() {
let mut corpus = corpus(query_with("returns_list", json!(true)), preview_quote());
corpus["query_defaults"] = json!({"where_clause": true, "order_by": true, "limit": true,
"offset": true, "pagination_order": "identity"});
let artifact = accepts(corpus);
let query = artifact.schema.queries.iter().find(|q| q.name == "quotePreview").unwrap();
assert!(
!query.auto_params.has_limit,
"a project default must not become an argument here"
);
assert!(!query.auto_params.has_where);
}
#[test]
fn a_query_naming_an_undeclared_function_is_refused() {
let message = refusal(corpus(query_with("function", json!("preview_qoute")), preview_quote()));
assert!(
message.contains("quotePreview") && message.contains("preview_qoute"),
"the refusal must name the query and the name it could not resolve; got: {message}"
);
}
#[test]
fn a_query_naming_a_function_with_no_functions_section_is_refused() {
let message = refusal(corpus(function_backed_query(), json!([])));
assert!(
message.contains("quotePreview") && message.contains("preview_quote"),
"a missing section is a missing function, not an absent question; got: {message}"
);
}
#[test]
fn a_query_naming_an_event_triggered_function_is_refused() {
let functions = json!([{"name": "preview_quote",
"trigger": "after:mutation:Quote:update",
"runtime": "Deno"}]);
let message = refusal(corpus(function_backed_query(), functions));
assert!(
message.contains("after:mutation:Quote:update") && message.contains("request:query"),
"the refusal must name the trigger it found and the one it needs; got: {message}"
);
}
#[test]
fn a_request_query_function_no_query_names_is_refused() {
let queries = json!([{"name": "quotes", "return_type": "Quote", "returns_list": true,
"nullable": false, "sql_source": "v_quote", "arguments": []}]);
let message = refusal(corpus(queries, preview_quote()));
assert!(
message.contains("preview_quote") && message.contains("function ="),
"the refusal must name the function and the declaration that would bind it; \
got: {message}"
);
}
#[test]
fn a_nested_field_declaring_a_function_is_refused() {
let mut corpus = corpus(function_backed_query(), preview_quote());
corpus["types"][0]["fields"][1]["function"] = json!("preview_quote");
let message = refusal(corpus);
assert!(
message.contains("total") && message.contains("root query field"),
"the refusal must name the field and state the rule; got: {message}"
);
assert!(
message.contains("per row"),
"and say why it is not a limitation to be lifted later; got: {message}"
);
}
#[test]
fn declaring_both_a_sql_source_and_a_function_is_refused() {
let message = refused_for_the_binding(corpus(
query_with("sql_source", json!("v_quote")),
preview_quote(),
));
assert!(
message.contains("quotePreview") && message.contains("resolves one way"),
"the refusal must say which query and why; got: {message}"
);
}
#[test]
fn relay_beside_a_function_is_refused() {
let mut queries = query_with("relay", json!(true));
queries[0]["returns_list"] = json!(true);
let message = refused_for_the_binding(corpus(queries, preview_quote()));
assert!(
message.contains("relay") && message.contains("keyset"),
"the refusal must be about the cursor column, not about sql_source; got: {message}"
);
}
#[test]
fn count_beside_a_function_is_refused() {
let mut queries = query_with("count", json!(true));
queries[0]["returns_list"] = json!(true);
let message = refused_for_the_binding(corpus(queries, preview_quote()));
assert!(
message.contains("count") && message.contains("COUNT(*)"),
"the refusal must be about the count statement; got: {message}"
);
}
#[test]
fn inject_params_beside_a_function_is_refused() {
let message = refused_for_the_binding(corpus(
query_with("inject_params", json!({"tenant_id": "jwt:tenant_id"})),
preview_quote(),
));
assert!(
message.contains("inject_params") && message.contains("scoping"),
"the refusal must say it is a scoping control, not merely unsupported; got: {message}"
);
}
#[test]
fn pagination_order_beside_a_function_is_refused() {
let mut queries = query_with("pagination_order", json!("created_at"));
queries[0]["returns_list"] = json!(true);
let message = refused_for_the_binding(corpus(queries, preview_quote()));
assert!(
message.contains("pagination_order") && message.contains("created_at"),
"the refusal must quote what was declared; got: {message}"
);
}
#[test]
fn rest_stream_beside_a_function_is_refused() {
let mut queries = query_with("rest_stream", json!(true));
queries[0]["returns_list"] = json!(true);
let message = refused_for_the_binding(corpus(queries, preview_quote()));
assert!(
message.contains("rest_stream"),
"the refusal must name the flag; got: {message}"
);
}
#[test]
fn a_rest_override_beside_a_function_is_refused() {
let message = refused_for_the_binding(corpus(
query_with("rest", json!({"path": "/quote-preview"})),
preview_quote(),
));
assert!(
message.contains("/quote-preview") && message.contains("REST"),
"the refusal must quote the route and name the surface; got: {message}"
);
}
#[test]
fn jsonb_column_beside_a_function_is_refused() {
let message = refused_for_the_binding(corpus(
query_with("jsonb_column", json!("payload")),
preview_quote(),
));
assert!(
message.contains("jsonb_column") && message.contains("payload"),
"the refusal must quote what was declared; got: {message}"
);
}
#[test]
fn read_routing_beside_a_function_is_refused() {
let message = refused_for_the_binding(corpus(
query_with("read_routing", json!("primary")),
preview_quote(),
));
assert!(
message.contains("read_routing") && message.contains("read bridge"),
"the refusal must say where the reads actually happen; got: {message}"
);
}
#[test]
fn a_declared_auto_param_beside_a_function_is_refused() {
let mut queries = query_with("auto_params", json!({"limit": true}));
queries[0]["returns_list"] = json!(true);
let message = refused_for_the_binding(corpus(queries, preview_quote()));
assert!(
message.contains("auto_params.limit"),
"the refusal must name the flag that was declared; got: {message}"
);
}
#[test]
fn additional_views_beside_a_function_is_refused() {
let message = refused_for_the_binding(corpus(
query_with("additional_views", json!(["v_price"])),
preview_quote(),
));
assert!(
message.contains("additional_views") && message.contains("v_price"),
"the refusal must quote what was declared; got: {message}"
);
}
#[test]
fn cache_ttl_seconds_beside_a_function_is_refused() {
let message = refused_for_the_binding(corpus(
query_with("cache_ttl_seconds", json!(300)),
preview_quote(),
));
assert!(
message.contains("cache_ttl_seconds") && message.contains("300"),
"the refusal must quote what was declared; got: {message}"
);
assert!(
message.contains("not cached"),
"and say what happens instead, rather than point at another cache key; got: {message}"
);
}
#[test]
fn authorization_settings_survive() {
let mut queries = function_backed_query();
queries[0]["requires_role"] = json!("sales");
queries[0]["requires_actor"] = json!(["human_user"]);
let artifact = accepts(corpus(queries, preview_quote()));
let query = artifact.schema.queries.iter().find(|q| q.name == "quotePreview").unwrap();
assert_eq!(query.requires_role.as_deref(), Some("sales"));
assert_eq!(query.requires_actor.len(), 1);
}
#[test]
fn two_queries_may_name_one_function() {
let mut queries = function_backed_query();
let mut second = queries[0].clone();
second["name"] = json!("bulkQuotePreview");
second["returns_list"] = json!(true);
second["nullable"] = json!(false);
queries.as_array_mut().unwrap().push(second);
let artifact = accepts(corpus(queries, preview_quote()));
assert_eq!(
artifact
.schema
.queries
.iter()
.filter(|q| q.function.as_deref() == Some("preview_quote"))
.count(),
2
);
}