Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab_schema_spec.rb « graphql « api « requests « spec - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 060a1b42cb6174567f1561f12ee069c39af4d490 (plain)
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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe 'GitlabSchema configurations', feature_category: :integrations 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]) }, # Complexity 4
        { query: graphql_query_for('echo', { 'text' => "$test" }, []), variables: { "test" => "Hello world" } }, # Complexity 1
        { query: graphql_query_for('project', { 'fullPath' => project.full_path }, "userPermissions { createIssue }") } # Complexity 3
      ]

      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

    shared_examples 'query is too complex' do |description, max_complexity|
      it description, :aggregate_failures do
        allow(GitlabSchema).to receive(:max_query_complexity).and_return max_complexity

        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(/Query has complexity of 8, which exceeds max complexity of #{max_complexity}/)
        end
      end
    end

    it_behaves_like 'imposing query limits' do
      # The total complexity of the multiplex query above is 8
      it_behaves_like 'query is too complex', 'fails all queries when only one of the queries is too complex', 4
      it_behaves_like 'query is too complex', 'fails when all queries combined are too complex', 7
    end

    context 'authentication' do
      let(:current_user) { project.first_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
      expect_any_instance_of(Gitlab::Graphql::QueryAnalyzers::AST::LoggerAnalyzer).to receive(:duration).and_return(7)

      expect(Gitlab::GraphqlLogger).to receive(:info).with(
        hash_including(
          trace_type: 'execute_query',
          "query_analysis.duration_s" => 7,
          "query_analysis.complexity" => 181,
          "query_analysis.depth" => 13,
          "query_analysis.used_deprecated_fields" => an_instance_of(Array),
          "query_analysis.used_fields" => an_instance_of(Array)
        )
      )

      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.first_owner)

      parsed_id = GlobalID.parse(graphql_data['project']['id'])

      expect(parsed_id).to eq(project.to_global_id)
    end
  end

  describe 'removal of deprecated items' do
    let(:mock_schema) do
      Class.new(GraphQL::Schema) do
        lazy_resolve ::Gitlab::Graphql::Lazy, :force

        query(Class.new(::Types::BaseObject) do
          graphql_name 'Query'

          field :foo, GraphQL::Types::Boolean,
                    deprecated: { milestone: '0.1', reason: :renamed }

          field :bar, (Class.new(::Types::BaseEnum) do
            graphql_name 'BarEnum'

            value 'FOOBAR', value: 'foobar', deprecated: { milestone: '0.1', reason: :renamed }
            value 'FOOBARNEW', value: 'foobarnew'
          end)

          field :baz, GraphQL::Types::Boolean do
            argument :arg, String, required: false, deprecated: { milestone: '0.1', reason: :renamed }
          end

          def foo
            false
          end

          def bar
            'foobar'
          end

          def baz(arg:)
            false
          end
        end)
      end
    end

    let(:params) { {} }
    let(:headers) { {} }

    before do
      allow(GitlabSchema).to receive(:execute).and_wrap_original do |method, *args, **kwargs|
        mock_schema.execute(*args, **kwargs)
      end
    end

    context 'without `remove_deprecated` param' do
      it 'shows deprecated items' do
        query = '{ foo bar baz(arg: "test") }'

        post_graphql(query, params: params, headers: headers)

        expect(json_response).to include('data' => { 'foo' => false, 'bar' => 'FOOBAR', 'baz' => false })
      end
    end

    context 'with `remove_deprecated` param' do
      let(:params) { { remove_deprecated: '1' } }

      it 'hides deprecated field' do
        query = '{ foo }'

        post_graphql(query, params: params)

        expect(json_response).not_to include('data' => { 'foo' => false })
        expect(json_response).to include(
          'errors' => include(a_hash_including('message' => /Field 'foo' doesn't exist on type 'Query'/))
        )
      end

      it 'hides deprecated enum value' do
        query = '{ bar }'

        post_graphql(query, params: params)

        expect(json_response).not_to include('data' => { 'bar' => 'FOOBAR' })
        expect(json_response).to include(
          'errors' => include(
            a_hash_including(
              'message' => /`Query.bar` returned `"foobar"` at `bar`, but this isn't a valid value for `BarEnum`/
            )
          )
        )
      end

      it 'hides deprecated argument' do
        query = '{ baz(arg: "test") }'

        post_graphql(query, params: params)

        expect(json_response).not_to include('data' => { 'bar' => 'FOOBAR' })
        expect(json_response).to include(
          'errors' => include(a_hash_including('message' => /Field 'baz' doesn't accept argument 'arg'/))
        )
      end
    end
  end
end