#![allow(clippy::unwrap_used, clippy::panic)]
use serde_json::json;
use super::*;
use crate::graphql::types::GraphQLType;
fn named(name: &str, nullable: bool) -> GraphQLType {
GraphQLType {
name: name.to_string(),
nullable,
list: false,
list_nullable: true,
}
}
fn var(name: &str, type_name: &str, nullable: bool) -> VariableDefinition {
VariableDefinition {
name: name.to_string(),
var_type: named(type_name, nullable),
default_value: None,
}
}
fn literal(name: &str, value_type: &str, value_json: &str) -> GraphQLArgument {
GraphQLArgument {
name: name.to_string(),
value_type: value_type.to_string(),
value_json: value_json.to_string(),
}
}
fn var_use(name: &str, var_name: &str) -> GraphQLArgument {
GraphQLArgument {
name: name.to_string(),
value_type: "variable".to_string(),
value_json: serde_json::to_string(&value_json::variable_ref(var_name)).unwrap(),
}
}
fn limit_arg() -> Vec<ArgumentDefinition> {
vec![ArgumentDefinition::optional("limit", FieldType::Int)]
}
fn message(result: Result<()>) -> String {
match result {
Err(FraiseQLError::Validation { message, .. }) => message,
other => panic!("expected a Validation error, got {other:?}"),
}
}
#[test]
fn an_int_literal_is_accepted_at_an_int_argument() {
let args = [literal("limit", "int", "2")];
assert!(validate_argument_values("Query.things", &limit_arg(), &args, &[]).is_ok());
}
#[test]
fn a_string_literal_is_refused_at_an_int_argument() {
let args = [literal("limit", "string", "\"2\"")];
let msg = message(validate_argument_values("Query.things", &limit_arg(), &args, &[]));
assert!(msg.contains("limit"), "message was: {msg}");
assert!(msg.contains("Int"), "message was: {msg}");
assert!(msg.contains("String"), "message was: {msg}");
}
#[test]
fn a_float_literal_is_refused_at_an_int_argument() {
let args = [literal("limit", "float", "2.5")];
assert!(validate_argument_values("Query.things", &limit_arg(), &args, &[]).is_err());
}
#[test]
fn a_boolean_literal_is_refused_at_an_int_argument() {
let args = [literal("limit", "boolean", "true")];
assert!(validate_argument_values("Query.things", &limit_arg(), &args, &[]).is_err());
}
#[test]
fn an_int_literal_past_the_32_bit_range_is_refused() {
let args = [literal("limit", "int", "99999999999999")];
assert!(validate_argument_values("Query.things", &limit_arg(), &args, &[]).is_err());
}
#[test]
fn an_explicit_null_is_accepted_because_nullability_is_not_adjudicated_here() {
let args = [literal("limit", "null", "null")];
assert!(validate_argument_values("Query.things", &limit_arg(), &args, &[]).is_ok());
}
#[test]
fn an_argument_the_field_does_not_declare_is_left_to_the_name_rule() {
let args = [literal("first", "string", "\"2\"")];
assert!(validate_argument_values("Query.things", &limit_arg(), &args, &[]).is_ok());
}
#[test]
fn a_json_argument_accepts_every_shape() {
let declared = vec![ArgumentDefinition::optional("where", FieldType::Json)];
for (kind, body) in [("string", "\"x\""), ("int", "1"), ("object", "{\"a\":1}")] {
let args = [literal("where", kind, body)];
assert!(
validate_argument_values("Query.things", &declared, &args, &[]).is_ok(),
"JSON argument refused a {kind} value"
);
}
}
#[test]
fn an_enum_literal_is_not_adjudicated_as_a_scalar() {
let declared = vec![ArgumentDefinition::optional("status", FieldType::String)];
let args = [literal("status", "enum", "\"PENDING\"")];
assert!(validate_argument_values("Query.things", &declared, &args, &[]).is_ok());
}
#[test]
fn a_string_literal_is_accepted_at_a_uuid_argument() {
let declared = vec![ArgumentDefinition::new("id", FieldType::Uuid)];
let args = [literal(
"id",
"string",
"\"11110000-0000-0000-0000-000000000001\"",
)];
assert!(validate_argument_values("Query.thing", &declared, &args, &[]).is_ok());
}
#[test]
fn an_int_literal_is_refused_at_a_uuid_argument() {
let declared = vec![ArgumentDefinition::new("id", FieldType::Uuid)];
let args = [literal("id", "int", "1")];
assert!(validate_argument_values("Query.thing", &declared, &args, &[]).is_err());
}
#[test]
fn an_int_declared_variable_is_usable_at_an_int_argument() {
let vars = [var("n", "Int", false)];
let args = [var_use("limit", "n")];
assert!(validate_argument_values("Query.things", &limit_arg(), &args, &vars).is_ok());
}
#[test]
fn a_string_declared_variable_is_refused_at_an_int_argument() {
let vars = [var("n", "String", false)];
let args = [var_use("limit", "n")];
let msg = message(validate_argument_values("Query.things", &limit_arg(), &args, &vars));
assert!(msg.contains("$n"), "message was: {msg}");
assert!(msg.contains("String"), "message was: {msg}");
assert!(msg.contains("Int"), "message was: {msg}");
}
#[test]
fn a_string_declared_variable_is_allowed_at_a_uuid_argument() {
let declared = vec![ArgumentDefinition::new("id", FieldType::Uuid)];
let vars = [var("id", "String", false)];
let args = [var_use("id", "id")];
assert!(validate_argument_values("Query.thing", &declared, &args, &vars).is_ok());
}
#[test]
fn a_variable_declared_at_a_type_the_schema_does_not_publish_is_not_adjudicated() {
let vars = [var("n", "SomeProjectScalar", false)];
let args = [var_use("limit", "n")];
assert!(validate_argument_values("Query.things", &limit_arg(), &args, &vars).is_ok());
}
#[test]
fn an_undeclared_variable_reference_is_left_to_the_5_8_3_rule() {
let args = [var_use("limit", "nowhere")];
assert!(validate_argument_values("Query.things", &limit_arg(), &args, &[]).is_ok());
}
#[test]
fn a_supplied_value_of_the_declared_type_is_accepted() {
let vars = [var("n", "Int", false)];
let values = json!({"n": 2});
assert!(validate_variable_values(Some("Q"), &vars, Some(&values)).is_ok());
}
#[test]
fn a_supplied_value_contradicting_its_declaration_is_refused() {
let vars = [var("n", "Int", false)];
let values = json!({"n": "2"});
let msg = message(validate_variable_values(Some("Q"), &vars, Some(&values)));
assert!(msg.contains("$n"), "message was: {msg}");
assert!(msg.contains("Int"), "message was: {msg}");
assert!(msg.contains('Q'), "message was: {msg}");
}
#[test]
fn a_non_null_variable_with_no_value_is_refused() {
let vars = [var("n", "Int", false)];
let msg = message(validate_variable_values(None, &vars, None));
assert!(msg.contains("$n"), "message was: {msg}");
assert!(msg.contains("no value was supplied"), "message was: {msg}");
}
#[test]
fn a_non_null_variable_supplied_as_null_is_refused() {
let vars = [var("n", "Int", false)];
let values = json!({"n": null});
assert!(validate_variable_values(None, &vars, Some(&values)).is_err());
}
#[test]
fn a_nullable_variable_with_no_value_is_accepted() {
let vars = [var("n", "Int", true)];
assert!(validate_variable_values(None, &vars, None).is_ok());
}
#[test]
fn a_non_null_variable_with_a_default_and_no_value_is_accepted() {
let mut v = var("n", "Int", false);
v.default_value = Some("10".to_string());
assert!(validate_variable_values(None, &[v], None).is_ok());
}
#[test]
fn a_list_declared_variable_is_not_adjudicated() {
let mut v = var("ids", "Int", true);
v.var_type.list = true;
let values = json!({"ids": [1, 2, 3]});
assert!(validate_variable_values(None, &[v], Some(&values)).is_ok());
}
#[test]
fn a_variable_of_a_project_scalar_is_not_adjudicated() {
let vars = [var("payload", "SomeProjectScalar", false)];
let values = json!({"payload": {"anything": true}});
assert!(validate_variable_values(None, &vars, Some(&values)).is_ok());
}
#[test]
fn an_out_of_range_int_says_so_rather_than_int_where_int_was_expected() {
let args = [literal("limit", "int", "99999999999999")];
let msg = message(validate_argument_values("Query.things", &limit_arg(), &args, &[]));
assert!(msg.contains("32-bit"), "message was: {msg}");
assert!(!msg.contains("a Int value"), "message was: {msg}");
}
use crate::schema::{
CompiledSchema, EnumDefinition, EnumValueDefinition, InputFieldDefinition,
InputObjectDefinition,
};
fn enum_schema() -> CompiledSchema {
let mut schema = CompiledSchema::new();
schema.enums.push(
EnumDefinition::new("OrderStatus")
.with_value(EnumValueDefinition::new("PENDING"))
.with_value(EnumValueDefinition::new("SHIPPED"))
.with_value(EnumValueDefinition::new("CANCELLED")),
);
schema.input_types.push(
InputObjectDefinition::new("CreateOrderInput")
.with_field(InputFieldDefinition::new("reference", "String"))
.with_field(InputFieldDefinition::new("status", "OrderStatus"))
.with_field(InputFieldDefinition::new("history", "[OrderStatus!]"))
.with_field(InputFieldDefinition::new("nested", "CreateOrderInput")),
);
schema
}
fn input_arg() -> Vec<ArgumentDefinition> {
vec![ArgumentDefinition::optional(
"input",
FieldType::Object("CreateOrderInput".to_string()),
)]
}
fn status_arg() -> Vec<ArgumentDefinition> {
vec![ArgumentDefinition::optional(
"status",
FieldType::Enum("OrderStatus".to_string()),
)]
}
#[test]
fn a_non_member_at_an_enum_input_field_is_refused() {
let schema = enum_schema();
let values = json!({"input": {"reference": "r-1", "status": "BANANA"}});
let err = message(validate_mutation_argument_values(
&schema,
"Mutation.createOrder",
&input_arg(),
Some(&values),
));
assert!(err.contains("OrderStatus"), "the refusal must name the enum: {err}");
assert!(err.contains("input.status"), "the refusal must name the path: {err}");
assert!(
err.contains("PENDING, SHIPPED, CANCELLED"),
"the refusal must name the members introspection already publishes: {err}"
);
assert!(
!err.contains("BANANA"),
"the offending value is described, never quoted back (#1197): {err}"
);
}
#[test]
fn a_member_at_an_enum_input_field_is_accepted() {
let schema = enum_schema();
let values = json!({"input": {"reference": "r-1", "status": "SHIPPED"}});
assert!(
validate_mutation_argument_values(&schema, "M.m", &input_arg(), Some(&values)).is_ok(),
"a declared member must pass"
);
}
#[test]
fn the_reported_non_member_spellings_are_each_refused() {
let schema = enum_schema();
for wrote in [
json!("BANANA"),
json!("pending"),
json!("anything at all"),
json!(42),
] {
let values = json!({"input": {"status": wrote}});
assert!(
validate_mutation_argument_values(&schema, "M.m", &input_arg(), Some(&values)).is_err(),
"{wrote} is not a member of OrderStatus and must be refused"
);
}
}
#[test]
fn a_non_member_inside_a_list_of_enums_is_refused() {
let schema = enum_schema();
let values = json!({"input": {"history": ["PENDING", "BANANA"]}});
let err =
message(validate_mutation_argument_values(&schema, "M.m", &input_arg(), Some(&values)));
assert!(err.contains("input.history[1]"), "the refusal must name the element: {err}");
}
#[test]
fn a_bare_non_member_written_at_a_list_of_enums_is_refused() {
let schema = enum_schema();
let values = json!({"input": {"history": "BANANA"}});
assert!(
validate_mutation_argument_values(&schema, "M.m", &input_arg(), Some(&values)).is_err()
);
}
#[test]
fn a_non_member_nested_one_input_object_deeper_is_refused() {
let schema = enum_schema();
let values = json!({"input": {"nested": {"status": "BANANA"}}});
let err =
message(validate_mutation_argument_values(&schema, "M.m", &input_arg(), Some(&values)));
assert!(err.contains("input.nested.status"), "the refusal must name the path: {err}");
}
#[test]
fn a_non_member_at_a_bare_enum_argument_is_refused() {
let schema = enum_schema();
let values = json!({"status": "BANANA"});
assert!(
validate_mutation_argument_values(&schema, "Q.q", &status_arg(), Some(&values)).is_err()
);
}
#[test]
fn an_absent_or_null_enum_field_is_left_to_the_required_field_rule() {
let schema = enum_schema();
for values in [
json!({"input": {"reference": "r"}}),
json!({"input": {"status": null}}),
] {
assert!(
validate_mutation_argument_values(&schema, "M.m", &input_arg(), Some(&values)).is_ok(),
"nullability is #414's question, not this one"
);
}
}
#[test]
fn a_field_whose_type_the_schema_does_not_declare_is_not_adjudicated() {
let mut schema = enum_schema();
schema.input_types.push(
InputObjectDefinition::new("Loose")
.with_field(InputFieldDefinition::new("whatever", "SomeProjectScalar")),
);
let declared = vec![ArgumentDefinition::optional(
"input",
FieldType::Object("Loose".to_string()),
)];
let values = json!({"input": {"whatever": "anything at all"}});
assert!(validate_mutation_argument_values(&schema, "M.m", &declared, Some(&values)).is_ok());
}
#[test]
fn a_non_member_written_as_an_inline_enum_literal_is_refused() {
let schema = enum_schema();
let args = [literal("status", "enum", "\"BANANA\"")];
assert!(
validate_enum_argument_literals(&schema, "Query.orders", &status_arg(), &args).is_err(),
"an inline literal must be adjudicated, not only a variable"
);
}
#[test]
fn a_member_written_as_an_inline_enum_literal_is_accepted() {
let schema = enum_schema();
let args = [literal("status", "enum", "\"SHIPPED\"")];
assert!(validate_enum_argument_literals(&schema, "Query.orders", &status_arg(), &args).is_ok());
}
#[test]
fn a_variable_reference_is_left_to_the_variable_check() {
let schema = enum_schema();
let args = [var_use("status", "s")];
assert!(validate_enum_argument_literals(&schema, "Query.orders", &status_arg(), &args).is_ok());
}
#[test]
fn a_non_member_supplied_for_an_enum_variable_is_refused() {
let schema = enum_schema();
let defs = [var("s", "OrderStatus", true)];
let values = json!({"s": "BANANA"});
let err = message(validate_enum_variable_values(&schema, Some("Op"), &defs, Some(&values)));
assert!(err.contains("$s"), "the refusal must name the variable: {err}");
assert!(err.contains("Op"), "the refusal must name the operation: {err}");
}
#[test]
fn a_non_member_supplied_inside_an_input_object_variable_is_refused() {
let schema = enum_schema();
let defs = [var("input", "CreateOrderInput", true)];
let values = json!({"input": {"status": "BANANA"}});
let err = message(validate_enum_variable_values(&schema, None, &defs, Some(&values)));
assert!(err.contains("status"), "the refusal must name the field: {err}");
}
#[test]
fn a_member_supplied_for_an_enum_variable_is_accepted() {
let schema = enum_schema();
let defs = [var("s", "OrderStatus", true)];
assert!(
validate_enum_variable_values(&schema, None, &defs, Some(&json!({"s": "PENDING"}))).is_ok()
);
}
#[test]
fn a_schema_declaring_no_enums_adjudicates_nothing() {
let schema = CompiledSchema::new();
let defs = [var("s", "OrderStatus", true)];
assert!(
validate_enum_variable_values(&schema, None, &defs, Some(&json!({"s": "BANANA"}))).is_ok()
);
}
#[test]
fn a_wrong_scalar_nested_in_a_mutation_input_is_refused_with_its_path() {
let schema = enum_schema();
let values = json!({"input": {"nested": {"reference": ["r"]}}});
let err =
message(validate_mutation_argument_values(&schema, "M.m", &input_arg(), Some(&values)));
assert!(err.contains("`input.nested.reference`"), "names the path: {err}");
assert!(
err.contains("`String`") && err.contains("wrote a list"),
"names the types: {err}"
);
}
#[test]
fn an_undeclared_key_in_a_mutation_input_is_refused() {
let schema = enum_schema();
let values = json!({"input": {"reference": "r", "colour": "red"}});
let err =
message(validate_mutation_argument_values(&schema, "M.m", &input_arg(), Some(&values)));
assert!(
err.contains("`input.colour`") && err.contains("declares no such field"),
"{err}"
);
}
#[test]
fn a_localized_input_field_is_left_to_its_own_pass() {
let mut schema = enum_schema();
let mut name = InputFieldDefinition::new("name", "String");
name.localized = true;
schema.input_types[0].fields.push(name);
let values = json!({"input": {"name": {"fr-FR": "Pomme"}}});
assert!(validate_mutation_argument_values(&schema, "M.m", &input_arg(), Some(&values)).is_ok());
}
#[test]
fn a_string_that_is_not_a_uuid_is_refused_at_a_uuid_argument() {
let declared = vec![ArgumentDefinition::optional("id", FieldType::Uuid)];
let args = [literal("id", "string", "\"x\"")];
let err = message(validate_argument_values("Query.thing", &declared, &args, &[]));
assert!(err.contains("not a UUID"), "{err}");
let ok = [literal(
"id",
"string",
"\"3f2504e0-4f89-11d3-9a0c-0305e82c3301\"",
)];
assert!(
validate_argument_values("Query.thing", &declared, &ok, &[]).is_ok(),
"a UUID passes"
);
}
#[test]
fn a_variable_marker_is_a_reference_only_in_a_document_literal() {
let schema = enum_schema();
let marker = json!({"status": {"$var": "s"}});
let reference = literal("input", "object", &marker.to_string());
assert!(
validate_enum_argument_literals(&schema, "Query.orders", &input_arg(), &[reference])
.is_ok(),
"a nested variable reference in a literal is the variable's to adjudicate"
);
let payload = json!({"input": marker});
assert!(
message(validate_mutation_argument_values(
&schema,
"Mutation.createOrder",
&input_arg(),
Some(&payload)
))
.contains("OrderStatus"),
"a marker-shaped object in a payload is data, refused at an enum position"
);
assert!(
message(validate_enum_variable_values(
&schema,
None,
&[var("i", "CreateOrderInput", true)],
Some(&json!({"i": marker}))
))
.contains("OrderStatus"),
"a marker-shaped object in a variable's value is data, refused at an enum position"
);
}
fn where_schema() -> (CompiledSchema, Vec<ArgumentDefinition>) {
let mut schema = CompiledSchema::new();
schema.input_types.push(
InputObjectDefinition::new("StringFilter")
.with_field(InputFieldDefinition::new("nin", "[String!]")),
);
schema.input_types.push(
InputObjectDefinition::new("LabelWhere")
.with_field(InputFieldDefinition::new("name", "StringFilter"))
.with_field(InputFieldDefinition::new("note", "String"))
.with_field(InputFieldDefinition::new("_and", "[LabelWhere!]")),
);
let declared = vec![ArgumentDefinition::optional(
"where",
FieldType::Object("LabelWhere".to_string()),
)];
(schema, declared)
}
fn list_items(where_value: &Value) -> Result<()> {
let (schema, declared) = where_schema();
validate_argument_list_items(
&schema,
|| "Query.labels".to_string(),
&declared,
|name| (name == "where").then_some(where_value),
)
}
#[test]
fn a_null_item_is_found_wherever_it_sits_in_a_resolved_argument() {
assert!(
message(list_items(&json!({"name": {"nin": [null, "first"]}})))
.contains("where.name.nin[0]"),
"an operand list under the argument's input object"
);
assert!(
message(list_items(&json!({"_and": [{"name": {"nin": ["first", null]}}]})))
.contains("where._and[0].name.nin[1]"),
"an operand list under a list of input objects"
);
assert!(
list_items(&json!({"note": null, "name": {"nin": ["first"]}})).is_ok(),
"a null field value is no list's item"
);
}
fn payment_schema() -> CompiledSchema {
let mut schema = CompiledSchema::new();
schema.input_types.push(
InputObjectDefinition::new("PayInput")
.with_field(InputFieldDefinition::new("amount", "Decimal"))
.with_field(InputFieldDefinition::new("splits", "[Decimal!]")),
);
schema
}
fn pay_arg() -> Vec<ArgumentDefinition> {
vec![ArgumentDefinition::optional(
"input",
FieldType::Object("PayInput".to_string()),
)]
}
fn parsed(text: &str) -> Value {
serde_json::from_str(text).unwrap()
}
#[test]
fn a_decimal_takes_a_string_or_an_exact_integer() {
let schema = payment_schema();
let pays = |amount: &str| {
let values =
parsed(&format!(r#"{{"input": {{"amount": {amount}, "splits": [{amount}]}}}}"#));
validate_mutation_argument_values(&schema, "Mutation.pay", &pay_arg(), Some(&values))
};
for held in [
r#""12345678901234567.89""#,
r#""-0.010""#,
"7",
"-9223372036854775808",
"18446744073709551615",
] {
assert!(pays(held).is_ok(), "{held}");
}
for rounded in ["1.5", "1e3", "1.0", "18446744073709551616"] {
let msg = message(pays(rounded));
assert!(msg.contains("input.amount") && msg.contains("Decimal"), "{rounded}: {msg}");
assert!(msg.contains("as a string"), "the refusal says how to write one: {msg}");
}
}
#[test]
fn a_decimal_argument_and_variable_take_the_same_values() {
let declared = vec![ArgumentDefinition::new("min", FieldType::Decimal)];
let literal_at = |text: &str, kind: &str| {
validate_argument_values("Query.orders", &declared, &[literal("min", kind, text)], &[])
};
assert!(literal_at("10", "int").is_ok());
assert!(literal_at(r#""10.25""#, "string").is_ok());
assert!(literal_at("10.25", "float").is_err(), "a Float literal is already rounded");
let vars = [var("min", "Decimal", false)];
let supplied = |text: &str| {
validate_variable_values(Some("Q"), &vars, Some(&parsed(&format!(r#"{{"min": {text}}}"#))))
};
assert!(supplied(r#""10.25""#).is_ok());
assert!(supplied("10").is_ok());
assert!(supplied("10.25").is_err());
}
#[test]
fn a_float_declared_variable_is_refused_at_a_decimal_argument() {
let declared = vec![ArgumentDefinition::new("min", FieldType::Decimal)];
let vars = [var("min", "Float", false)];
let msg = message(validate_argument_values(
"Query.orders",
&declared,
&[var_use("min", "min")],
&vars,
));
assert!(msg.contains("$min") && msg.contains("Float"), "{msg}");
}