Repository: mirumee/ariadne

Issue:
Handle optional args in query cost multiplers
Query cost validator supports complexity multipliers which multiply given complexity by sum of specified values:

```graphql
type Query {
    news(promoted: Int, regular: Int): Post @cost(complexity: 1, multipliers: ["promoted", "regular"])
}
```

In situation when one of those arguments is skipped, its passed to complexity calculation as `None`, causing the `unsupported operand type(s) for +: 'int' and 'NoneType'` error within validator.

Relevant lines:

https://github.com/mirumee/ariadne/blob/master/ariadne/validation/query_cost.py#L283
https://github.com/mirumee/ariadne/blob/master/ariadne/validation/query_cost.py#L187

We should default missing variables to 0, so they are filtered out at next line. We should also consider filtering off negative numbers.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 478164c..87a6827 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
 - Made `Query` type optional in federated schemas.
 - Updated default resolvers to test for `Mapping` instead of `dict`.
 - Removed `ariadne.contrib.django`. (Use [ariadne_django](https://github.com/reset-button/ariadne_django) instead).
+- Updated query cost validator to handle optional variables.
 
 
 ## 0.13.0 (2021-03-17)
diff --git a/ariadne/validation/query_cost.py b/ariadne/validation/query_cost.py
index 4e74c74..92691d2 100644
--- a/ariadne/validation/query_cost.py
+++ b/ariadne/validation/query_cost.py
@@ -281,12 +281,15 @@ class CostValidator(ValidationRule):
             val = field_args
             for key in accessor:
                 val = val.get(key)
-            multipliers.append(val)
+            try:
+                multipliers.append(int(val))  # type: ignore
+            except (ValueError, TypeError):
+                pass
         multipliers = [
             len(multiplier) if isinstance(multiplier, (list, tuple)) else multiplier
             for multiplier in multipliers
         ]
-        return [m for m in multipliers if m != 0]
+        return [m for m in multipliers if m > 0]  # type: ignore
 
     def get_cost_exceeded_error(self) -> GraphQLError:
         return GraphQLError(


Test patch:
diff --git a/tests/test_query_cost_validation.py b/tests/test_query_cost_validation.py
index ff1c0a9..692743f 100644
--- a/tests/test_query_cost_validation.py
+++ b/tests/test_query_cost_validation.py
@@ -22,7 +22,7 @@ def schema():
         type Query {
             constant: Int!
             simple(value: Int!): Int!
-            complex(valueA: Int!, valueB: Int!): Int!
+            complex(valueA: Int, valueB: Int): Int!
             nested(value: NestedInput!): Int!
             child(value: Int!): [Child!]!
         }
@@ -46,7 +46,7 @@ def schema_with_costs():
         type Query {
             constant: Int! @cost(complexity: 3)
             simple(value: Int!): Int! @cost(complexity: 1, multipliers: ["value"])
-            complex(valueA: Int!, valueB: Int!): Int! @cost(complexity: 1, multipliers: ["valueA", "valueB"])
+            complex(valueA: Int, valueB: Int): Int! @cost(complexity: 1, multipliers: ["valueA", "valueB"])
             noComplexity(value: Int!): Int! @cost(multipliers: ["value"])
             nested(value: NestedInput!): Int! @cost(complexity: 1, multipliers: ["value.num"])
             child(value: Int!): [Child!]! @cost(complexity: 1, multipliers: ["value"])
@@ -262,7 +262,7 @@ def test_complex_field_cost_defined_in_map_is_multiplied_by_values_from_variable
     schema,
 ):
     query = """
-        query testQuery($valueA: Int!, $valueB: Int!) {
+        query testQuery($valueA: Int, $valueB: Int) {
             complex(valueA: $valueA, valueB: $valueB)
         }
     """
@@ -292,11 +292,81 @@ def test_complex_field_cost_defined_in_map_is_multiplied_by_values_from_literal(
     ]
 
 
+def test_complex_field_cost_multiplication_by_values_from_variables_handles_nulls(
+    schema,
+):
+    query = """
+        query testQuery($valueA: Int, $valueB: Int) {
+            complex(valueA: $valueA, valueB: $valueB)
+        }
+    """
+    ast = parse(query)
+    rule = cost_validator(
+        maximum_cost=3, variables={"valueA": 5, "valueB": None}, cost_map=cost_map
+    )
+    result = validate(schema, ast, [rule])
+    assert result == [
+        GraphQLError(
+            "The query exceeds the maximum cost of 3. Actual cost is 5",
+            extensions={"cost": {"requestedQueryCost": 5, "maximumAvailable": 3}},
+        )
+    ]
+
+
+def test_complex_field_cost_multiplication_by_values_from_literals_handles_nulls(
+    schema,
+):
+    query = "{ complex(valueA: 5, valueB: null) }"
+    ast = parse(query)
+    rule = cost_validator(maximum_cost=3, cost_map=cost_map)
+    result = validate(schema, ast, [rule])
+    assert result == [
+        GraphQLError(
+            "The query exceeds the maximum cost of 3. Actual cost is 5",
+            extensions={"cost": {"requestedQueryCost": 5, "maximumAvailable": 3}},
+        )
+    ]
+
+
+def test_complex_field_cost_multiplication_by_values_from_variables_handles_optional(
+    schema,
+):
+    query = """
+        query testQuery($valueA: Int) {
+            complex(valueA: $valueA)
+        }
+    """
+    ast = parse(query)
+    rule = cost_validator(maximum_cost=3, variables={"valueA": 5}, cost_map=cost_map)
+    result = validate(schema, ast, [rule])
+    assert result == [
+        GraphQLError(
+            "The query exceeds the maximum cost of 3. Actual cost is 5",
+            extensions={"cost": {"requestedQueryCost": 5, "maximumAvailable": 3}},
+        )
+    ]
+
+
+def test_complex_field_cost_multiplication_by_values_from_literals_handles_optional(
+    schema,
+):
+    query = "{ complex(valueA: 5) }"
+    ast = parse(query)
+    rule = cost_validator(maximum_cost=3, cost_map=cost_map)
+    result = validate(schema, ast, [rule])
+    assert result == [
+        GraphQLError(
+            "The query exceeds the maximum cost of 3. Actual cost is 5",
+            extensions={"cost": {"requestedQueryCost": 5, "maximumAvailable": 3}},
+        )
+    ]
+
+
 def test_complex_field_cost_defined_in_directive_is_multiplied_by_values_from_variables(
     schema_with_costs,
 ):
     query = """
-        query testQuery($valueA: Int!, $valueB: Int!) {
+        query testQuery($valueA: Int, $valueB: Int) {
             complex(valueA: $valueA, valueB: $valueB)
         }
     """