1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
|
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'GitlabSchema configurations' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
shared_examples 'imposing query limits' do
describe 'timeouts' do
context 'when timeout is reached' do
it 'shows an error' do
allow_any_instance_of(Gitlab::Graphql::Timeout).to receive(:max_seconds).and_return(0)
subject
expect_graphql_errors_to_include /Timeout/
end
end
end
describe '#max_complexity' do
context 'when complexity is too high' do
it 'shows an error' do
allow(GitlabSchema).to receive(:max_query_complexity).and_return 1
subject
expect_graphql_errors_to_include /which exceeds max complexity of 1/
end
end
end
describe '#max_depth' do
context 'when query depth is too high' do
it 'shows error' do
allow(GitlabSchema).to receive(:max_query_depth).and_return 1
subject
expect_graphql_errors_to_include /exceeds max depth/
end
end
context 'when query depth is within range' do
it 'has no error' do
allow(GitlabSchema).to receive(:max_query_depth).and_return 5
subject
expect_graphql_errors_to_be_empty
end
end
end
end
context 'depth, complexity and recursion checking' do
context 'unauthenticated recursive queries' do
context 'a not-quite-recursive-enough introspective query' do
it 'succeeds' do
query = File.read(Rails.root.join('spec/fixtures/api/graphql/small-recursive-introspection.graphql'))
post_graphql(query, current_user: nil)
expect_graphql_errors_to_be_empty
end
end
context 'failing queries' do
before do
allow(GitlabSchema).to receive(:max_query_recursion).and_return 1
end
context 'a recursive introspective query' do
it 'fails due to recursion' do
query = File.read(Rails.root.join('spec/fixtures/api/graphql/recursive-introspection.graphql'))
post_graphql(query, current_user: nil)
expect_graphql_errors_to_include [/Recursive query/]
end
end
context 'a recursive non-introspective query' do
before do
allow(GitlabSchema).to receive(:max_query_complexity).and_return 1
allow(GitlabSchema).to receive(:max_query_depth).and_return 1
allow(GitlabSchema).to receive(:max_query_complexity).and_return 1
end
shared_examples 'fails due to recursion, complexity and depth' do |fixture_file|
it 'fails due to recursion, complexity and depth' do
query = File.read(Rails.root.join(fixture_file))
post_graphql(query, current_user: nil)
expect_graphql_errors_to_include [/Recursive query/, /exceeds max complexity/, /exceeds max depth/]
end
end
context 'using `nodes` notation' do
it_behaves_like 'fails due to recursion, complexity and depth', 'spec/fixtures/api/graphql/recursive-query-nodes.graphql'
end
context 'using `edges -> node` notation' do
it_behaves_like 'fails due to recursion, complexity and depth', 'spec/fixtures/api/graphql/recursive-query-edges-node.graphql'
end
end
end
end
end
context 'regular queries' do
subject do
query = graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description))
post_graphql(query)
end
it_behaves_like 'imposing query limits'
end
context 'multiplexed queries' do
let(:current_user) { nil }
subject do
queries = [
{ query: graphql_query_for('project', { 'fullPath' => '$fullPath' }, %w(id name description)) },
{ query: graphql_query_for('echo', { 'text' => "$test" }, []), variables: { "test" => "Hello world" } },
{ query: graphql_query_for('project', { 'fullPath' => project.full_path }, "userPermissions { createIssue }") }
]
post_multiplex(queries, current_user: current_user)
end
it 'does not authenticate all queries' do
subject
expect(json_response.last['data']['project']).to be_nil
end
it_behaves_like 'imposing query limits' do
it 'fails all queries when only one of the queries is too complex' do
# The `project` query above has a complexity of 5
allow(GitlabSchema).to receive(:max_query_complexity).and_return 4
subject
# Expect a response for each query, even though it will be empty
expect(json_response.size).to eq(3)
json_response.each do |single_query_response|
expect(single_query_response).not_to have_key('data')
end
# Expect errors for each query
expect(graphql_errors.size).to eq(3)
graphql_errors.each do |single_query_errors|
expect_graphql_errors_to_include(/which exceeds max complexity of 4/)
end
end
end
context 'authentication' do
let(:current_user) { project.owner }
it 'authenticates all queries' do
subject
expect(json_response.last['data']['project']['userPermissions']['createIssue']).to be(true)
end
end
end
context 'when IntrospectionQuery' do
it 'is not too complex nor recursive' do
query = File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql'))
post_graphql(query, current_user: nil)
expect_graphql_errors_to_be_empty
end
end
context 'logging' do
let(:query) { File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql')) }
it 'logs the query complexity and depth' do
analyzer_memo = {
query_string: query,
variables: {}.to_s,
complexity: 181,
depth: 13,
duration_s: 7,
used_fields: an_instance_of(Array),
used_deprecated_fields: an_instance_of(Array)
}
expect_any_instance_of(Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer).to receive(:duration).and_return(7)
expect(Gitlab::GraphqlLogger).to receive(:info).with(analyzer_memo)
post_graphql(query, current_user: nil)
end
it 'logs using `format_message`' do
expect_any_instance_of(Gitlab::GraphqlLogger).to receive(:format_message)
post_graphql(query, current_user: nil)
end
end
context "global id's" do
it 'uses GlobalID to expose ids' do
post_graphql(graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id)),
current_user: project.owner)
parsed_id = GlobalID.parse(graphql_data['project']['id'])
expect(parsed_id).to eq(project.to_global_id)
end
end
end
|