1 package complexity
2
3 import (
4 "math"
5 "testing"
6
7 "github.com/stretchr/testify/require"
8 "github.com/vektah/gqlparser/v2"
9 "github.com/vektah/gqlparser/v2/ast"
10
11 "github.com/99designs/gqlgen/graphql"
12 )
13
14 var schema = gqlparser.MustLoadSchema(
15 &ast.Source{
16 Name: "test.graphql",
17 Input: `
18 interface NameInterface {
19 name: String
20 }
21
22 type Item implements NameInterface {
23 scalar: String
24 name: String
25 list(size: Int = 10): [Item]
26 }
27
28 type ExpensiveItem implements NameInterface {
29 name: String
30 }
31
32 type Named {
33 name: String
34 }
35
36 union NameUnion = Item | Named
37
38 type Query {
39 scalar: String
40 object: Item
41 interface: NameInterface
42 union: NameUnion
43 customObject: Item
44 list(size: Int = 10): [Item]
45 }
46 `,
47 },
48 )
49
50 func requireComplexity(t *testing.T, source string, complexity int) {
51 t.Helper()
52 query := gqlparser.MustLoadQuery(schema, source)
53
54 es := &graphql.ExecutableSchemaMock{
55 ComplexityFunc: func(typeName, field string, childComplexity int, args map[string]interface{}) (int, bool) {
56 switch typeName + "." + field {
57 case "ExpensiveItem.name":
58 return 5, true
59 case "Query.list", "Item.list":
60 return int(args["size"].(int64)) * childComplexity, true
61 case "Query.customObject":
62 return 1, true
63 }
64 return 0, false
65 },
66 SchemaFunc: func() *ast.Schema {
67 return schema
68 },
69 }
70
71 actualComplexity := Calculate(es, query.Operations[0], nil)
72 require.Equal(t, complexity, actualComplexity)
73 }
74
75 func TestCalculate(t *testing.T) {
76 t.Run("uses default cost", func(t *testing.T) {
77 const query = `
78 {
79 scalar
80 }
81 `
82 requireComplexity(t, query, 1)
83 })
84
85 t.Run("adds together fields", func(t *testing.T) {
86 const query = `
87 {
88 scalar1: scalar
89 scalar2: scalar
90 }
91 `
92 requireComplexity(t, query, 2)
93 })
94
95 t.Run("a level of nesting adds complexity", func(t *testing.T) {
96 const query = `
97 {
98 object {
99 scalar
100 }
101 }
102 `
103 requireComplexity(t, query, 2)
104 })
105
106 t.Run("adds together children", func(t *testing.T) {
107 const query = `
108 {
109 scalar
110 object {
111 scalar
112 }
113 }
114 `
115 requireComplexity(t, query, 3)
116 })
117
118 t.Run("adds inline fragments", func(t *testing.T) {
119 const query = `
120 {
121 ... {
122 scalar
123 }
124 }
125 `
126 requireComplexity(t, query, 1)
127 })
128
129 t.Run("adds fragments", func(t *testing.T) {
130 const query = `
131 {
132 ... Fragment
133 }
134
135 fragment Fragment on Query {
136 scalar
137 }
138 `
139 requireComplexity(t, query, 1)
140 })
141
142 t.Run("uses custom complexity", func(t *testing.T) {
143 const query = `
144 {
145 list {
146 scalar
147 }
148 }
149 `
150 requireComplexity(t, query, 10)
151 })
152
153 t.Run("ignores negative custom complexity values", func(t *testing.T) {
154 const query = `
155 {
156 list(size: -100) {
157 scalar
158 }
159 }
160 `
161 requireComplexity(t, query, 2)
162 })
163
164 t.Run("custom complexity must be >= child complexity", func(t *testing.T) {
165 const query = `
166 {
167 customObject {
168 list(size: 100) {
169 scalar
170 }
171 }
172 }
173 `
174 requireComplexity(t, query, 101)
175 })
176
177 t.Run("interfaces take max concrete cost", func(t *testing.T) {
178 const query = `
179 {
180 interface {
181 name
182 }
183 }
184 `
185 requireComplexity(t, query, 6)
186 })
187
188 t.Run("guards against integer overflow", func(t *testing.T) {
189 if maxInt == math.MaxInt32 {
190
191 t.Skip()
192 }
193 const query = `
194 {
195 list1: list(size: 2147483647) {
196 list(size: 2147483647) {
197 list(size: 2) {
198 scalar
199 }
200 }
201 }
202 # total cost so far: 2*0x7fffffff*0x7fffffff
203 # = 0x7ffffffe00000002
204 # Adding the same again should cause overflow
205 list2: list(size: 2147483647) {
206 list(size: 2147483647) {
207 list(size: 2) {
208 scalar
209 }
210 }
211 }
212 }
213 `
214 requireComplexity(t, query, math.MaxInt64)
215 })
216 }
217
View as plain text