zenspec 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zenspec
4
+ module Helpers
5
+ module GraphQLHelpers
6
+ # Execute a GraphQL query with variables and context
7
+ #
8
+ # @param query [String] The GraphQL query string
9
+ # @param variables [Hash] Query variables
10
+ # @param context [Hash] Execution context
11
+ # @param schema [GraphQL::Schema] Optional schema (defaults to AppSchema)
12
+ # @return [GraphQL::Query::Result] The query result
13
+ #
14
+ # @example
15
+ # result = graphql_execute(query, variables: { id: "123" }, context: { current_user: user })
16
+ # expect(result).to succeed_graphql
17
+ #
18
+ def graphql_execute(query, variables: {}, context: {}, schema: nil)
19
+ schema_class = schema || default_schema
20
+ raise "No GraphQL schema found. Please define AppSchema or pass schema: argument" unless schema_class
21
+
22
+ schema_class.execute(
23
+ query,
24
+ variables: variables,
25
+ context: context
26
+ )
27
+ end
28
+
29
+ # Execute a GraphQL query with a user automatically added to context
30
+ #
31
+ # @param user [Object] The user object to add to context
32
+ # @param query [String] The GraphQL query string
33
+ # @param variables [Hash] Query variables
34
+ # @param context [Hash] Additional context (merged with user context)
35
+ # @param schema [GraphQL::Schema] Optional schema (defaults to AppSchema)
36
+ # @param context_key [Symbol] The key to use for the user in context (defaults to :current_user)
37
+ # @return [GraphQL::Query::Result] The query result
38
+ #
39
+ # @example
40
+ # result = graphql_execute_as(user, query, variables: { id: "123" })
41
+ # expect(result).to succeed_graphql
42
+ #
43
+ def graphql_execute_as(user, query, variables: {}, context: {}, schema: nil, context_key: :current_user)
44
+ merged_context = context.merge(context_key => user)
45
+ graphql_execute(query, variables: variables, context: merged_context, schema: schema)
46
+ end
47
+
48
+ # Execute a GraphQL mutation with input
49
+ #
50
+ # @param mutation [String] The GraphQL mutation string
51
+ # @param input [Hash] Mutation input variables
52
+ # @param context [Hash] Execution context
53
+ # @param schema [GraphQL::Schema] Optional schema (defaults to AppSchema)
54
+ # @return [GraphQL::Query::Result] The mutation result
55
+ #
56
+ # @example
57
+ # result = graphql_mutate(mutation, input: { name: "John", email: "john@example.com" })
58
+ # expect(result).to succeed_graphql
59
+ #
60
+ def graphql_mutate(mutation, input: {}, context: {}, schema: nil)
61
+ graphql_execute(mutation, variables: { input: input }, context: context, schema: schema)
62
+ end
63
+
64
+ # Execute a GraphQL mutation as a specific user
65
+ #
66
+ # @param user [Object] The user object to add to context
67
+ # @param mutation [String] The GraphQL mutation string
68
+ # @param input [Hash] Mutation input variables
69
+ # @param context [Hash] Additional context (merged with user context)
70
+ # @param schema [GraphQL::Schema] Optional schema (defaults to AppSchema)
71
+ # @param context_key [Symbol] The key to use for the user in context (defaults to :current_user)
72
+ # @return [GraphQL::Query::Result] The mutation result
73
+ #
74
+ # @example
75
+ # result = graphql_mutate_as(user, mutation, input: { name: "John" })
76
+ # expect(result).to succeed_graphql
77
+ #
78
+ def graphql_mutate_as(user, mutation, input: {}, context: {}, schema: nil, context_key: :current_user)
79
+ merged_context = context.merge(context_key => user)
80
+ graphql_mutate(mutation, input: input, context: merged_context, schema: schema)
81
+ end
82
+
83
+ private
84
+
85
+ def default_schema
86
+ # Try to find a schema in common locations
87
+ if defined?(AppSchema)
88
+ AppSchema
89
+ elsif defined?(Schema)
90
+ Schema
91
+ elsif defined?(GraphqlSchema)
92
+ GraphqlSchema
93
+ elsif defined?(ApplicationSchema)
94
+ ApplicationSchema
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ RSpec.configure do |config|
102
+ config.include Zenspec::Helpers::GraphQLHelpers
103
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helpers/graphql_helpers"
4
+
5
+ module Zenspec
6
+ module Helpers
7
+ # This module serves as a namespace for all Zenspec helpers
8
+ # Individual helper modules are loaded from their respective files
9
+ end
10
+ end
@@ -0,0 +1,358 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/expectations"
4
+
5
+ module Zenspec
6
+ module Matchers
7
+ module GraphQLMatchers
8
+ # Matcher for executing GraphQL queries and checking results
9
+ #
10
+ # @example
11
+ # expect(query).to execute_graphql
12
+ # expect(query).to execute_graphql.with_variables(id: "123")
13
+ # expect(query).to execute_graphql.with_context(current_user: user)
14
+ # expect(query).to execute_graphql.and_succeed
15
+ # expect(query).to execute_graphql.and_succeed.returning_data("user", "id")
16
+ #
17
+ RSpec::Matchers.define :execute_graphql do
18
+ match do |query_string|
19
+ @variables ||= {}
20
+ @context ||= {}
21
+
22
+ # Execute the query
23
+ schema_class = defined?(AppSchema) ? AppSchema : GraphQL::Schema
24
+ @result = schema_class.execute(
25
+ query_string,
26
+ variables: @variables,
27
+ context: @context
28
+ )
29
+
30
+ # Check if we need to verify success
31
+ if @check_success
32
+ errors = @result["errors"]
33
+ return false if !errors.nil? && (errors.is_a?(Array) ? !errors.empty? : true)
34
+ end
35
+
36
+ # Check if we need to return specific data
37
+ if @data_path&.any?
38
+ @returned_data = dig_data(@result["data"], @data_path)
39
+ true
40
+ else
41
+ @result["errors"].nil? || @result["errors"].empty?
42
+ end
43
+ end
44
+
45
+ chain :with_variables do |variables|
46
+ @variables = variables
47
+ end
48
+
49
+ chain :with_context do |context|
50
+ @context = context
51
+ end
52
+
53
+ chain :and_succeed do
54
+ @check_success = true
55
+ end
56
+
57
+ chain :returning_data do |*path|
58
+ @data_path = path
59
+ end
60
+
61
+ failure_message do
62
+ errors = @result["errors"]
63
+ has_errors = !errors.nil? && (errors.is_a?(Array) ? !errors.empty? : true)
64
+
65
+ if has_errors
66
+ "expected GraphQL query to succeed, but got errors: #{@result["errors"]}"
67
+ elsif @data_path
68
+ "expected to find data at path #{@data_path.inspect}, but result was: #{@result["data"].inspect}"
69
+ else
70
+ "expected GraphQL query to execute without errors, but got: #{@result["errors"]}"
71
+ end
72
+ end
73
+
74
+ # Allow accessing the returned data
75
+ def returned_data
76
+ @returned_data
77
+ end
78
+
79
+ private
80
+
81
+ def dig_data(data, path)
82
+ path.reduce(data) do |current, key|
83
+ return nil if current.nil?
84
+
85
+ current.is_a?(Hash) ? current[key.to_s] : nil
86
+ end
87
+ end
88
+ end
89
+
90
+ # Matcher for checking if GraphQL result has data at a specific path
91
+ #
92
+ # @example
93
+ # expect(result).to have_graphql_data
94
+ # expect(result).to have_graphql_data("user")
95
+ # expect(result).to have_graphql_data("user", "id")
96
+ # expect(result).to have_graphql_data("user", "id").with_value("123")
97
+ # expect(result).to have_graphql_data("user").matching(id: "123", name: "John")
98
+ # expect(result).to have_graphql_data("user").that_includes(name: "John")
99
+ # expect(result).to have_graphql_data("users").that_is_present
100
+ # expect(result).to have_graphql_data("deletedAt").that_is_null
101
+ # expect(result).to have_graphql_data("users").with_count(5)
102
+ #
103
+ RSpec::Matchers.define :have_graphql_data do |*path|
104
+ match do |result|
105
+ data = path.empty? ? result["data"] : dig_data(result["data"], path)
106
+
107
+ # Check for explicit null
108
+ if @check_null
109
+ return data.nil?
110
+ end
111
+
112
+ # Check for presence (not nil and not empty)
113
+ if @check_present
114
+ return !data.nil? && (data.respond_to?(:empty?) ? !data.empty? : true)
115
+ end
116
+
117
+ # Check count for arrays
118
+ if @expected_count
119
+ return false unless data.is_a?(Array)
120
+ return data.length == @expected_count
121
+ end
122
+
123
+ # Check for partial matching (subset)
124
+ if @expected_match
125
+ return false unless data.is_a?(Hash)
126
+ return @expected_match.all? { |key, value| data[key.to_s] == value }
127
+ end
128
+
129
+ # Check for inclusion (subset with any match)
130
+ if @expected_include
131
+ return false unless data.is_a?(Hash) || data.is_a?(Array)
132
+
133
+ if data.is_a?(Hash)
134
+ return @expected_include.all? { |key, value| data[key.to_s] == value }
135
+ else
136
+ # For arrays, check if any element matches
137
+ return data.any? do |item|
138
+ item.is_a?(Hash) && @expected_include.all? { |key, value| item[key.to_s] == value }
139
+ end
140
+ end
141
+ end
142
+
143
+ # Check exact value
144
+ if @expected_value
145
+ data == @expected_value
146
+ else
147
+ !data.nil?
148
+ end
149
+ end
150
+
151
+ chain :with_value do |expected_value|
152
+ @expected_value = expected_value
153
+ end
154
+
155
+ chain :matching do |expected_hash|
156
+ @expected_match = expected_hash
157
+ end
158
+
159
+ chain :that_includes do |expected_hash|
160
+ @expected_include = expected_hash
161
+ end
162
+
163
+ chain :that_is_present do
164
+ @check_present = true
165
+ end
166
+
167
+ chain :that_is_null do
168
+ @check_null = true
169
+ end
170
+
171
+ chain :with_count do |expected_count|
172
+ @expected_count = expected_count
173
+ end
174
+
175
+ failure_message do |result|
176
+ data = path.empty? ? result["data"] : dig_data(result["data"], path)
177
+
178
+ if @check_null
179
+ "expected data at #{path.inspect} to be null, but got: #{data.inspect}"
180
+ elsif @check_present
181
+ "expected data at #{path.inspect} to be present, but it was #{data.inspect}"
182
+ elsif @expected_count
183
+ actual_count = data.is_a?(Array) ? data.length : "not an array"
184
+ "expected data at #{path.inspect} to have count #{@expected_count}, got #{actual_count}"
185
+ elsif @expected_match
186
+ mismatches = @expected_match.reject { |key, value| data.is_a?(Hash) && data[key.to_s] == value }
187
+ "expected data at #{path.inspect} to match #{@expected_match.inspect}, " \
188
+ "but #{mismatches.inspect} did not match. Got: #{data.inspect}"
189
+ elsif @expected_include
190
+ "expected data at #{path.inspect} to include #{@expected_include.inspect}, got: #{data.inspect}"
191
+ elsif path.empty?
192
+ "expected result to have data, but got: #{result.inspect}"
193
+ elsif @expected_value
194
+ "expected data at #{path.inspect} to be #{@expected_value.inspect}, got #{data.inspect}"
195
+ else
196
+ "expected to find data at path #{path.inspect}, but result was: #{result["data"].inspect}"
197
+ end
198
+ end
199
+
200
+ def dig_data(data, path)
201
+ path.reduce(data) do |current, key|
202
+ return nil if current.nil?
203
+
204
+ current.is_a?(Hash) ? current[key.to_s] : nil
205
+ end
206
+ end
207
+ end
208
+
209
+ # Matcher for checking if GraphQL result has errors
210
+ #
211
+ # @example
212
+ # expect(result).to have_graphql_errors
213
+ # expect(result).to have_graphql_error.with_message("Not found")
214
+ # expect(result).to have_graphql_error.with_extensions(code: "NOT_FOUND")
215
+ # expect(result).to have_graphql_error.at_path(["user", "email"])
216
+ # expect { execute_query }.to have_graphql_errors
217
+ #
218
+ RSpec::Matchers.define :have_graphql_errors do
219
+ match do |result_or_block|
220
+ if result_or_block.is_a?(Proc)
221
+ begin
222
+ result_or_block.call
223
+ @raised_error = false
224
+ false
225
+ rescue StandardError => e
226
+ @raised_error = true
227
+ @error = e
228
+ true
229
+ end
230
+ else
231
+ @result = result_or_block
232
+ errors = result_or_block["errors"]
233
+ return false if errors.nil? || (errors.is_a?(Array) && errors.empty?)
234
+
235
+ # Check for specific message
236
+ if @expected_message
237
+ return errors.any? { |error| error["message"]&.include?(@expected_message) }
238
+ end
239
+
240
+ # Check for specific extensions
241
+ if @expected_extensions
242
+ return errors.any? do |error|
243
+ extensions = error["extensions"]
244
+ extensions && @expected_extensions.all? { |key, value| extensions[key.to_s] == value }
245
+ end
246
+ end
247
+
248
+ # Check for specific path
249
+ if @expected_path
250
+ return errors.any? { |error| error["path"] == @expected_path }
251
+ end
252
+
253
+ true
254
+ end
255
+ end
256
+
257
+ chain :with_message do |expected_message|
258
+ @expected_message = expected_message
259
+ end
260
+
261
+ chain :with_extensions do |expected_extensions|
262
+ @expected_extensions = expected_extensions
263
+ end
264
+
265
+ chain :at_path do |expected_path|
266
+ @expected_path = expected_path
267
+ end
268
+
269
+ failure_message do
270
+ if @raised_error
271
+ "expected no errors, but got: #{@error.message}"
272
+ elsif @expected_message
273
+ errors = @result["errors"] || []
274
+ messages = errors.map { |e| e["message"] }
275
+ "expected errors to include message containing #{@expected_message.inspect}, " \
276
+ "got messages: #{messages.inspect}"
277
+ elsif @expected_extensions
278
+ errors = @result["errors"] || []
279
+ extensions = errors.map { |e| e["extensions"] }
280
+ "expected errors to have extensions #{@expected_extensions.inspect}, " \
281
+ "got extensions: #{extensions.inspect}"
282
+ elsif @expected_path
283
+ errors = @result["errors"] || []
284
+ paths = errors.map { |e| e["path"] }
285
+ "expected errors at path #{@expected_path.inspect}, got paths: #{paths.inspect}"
286
+ else
287
+ "expected GraphQL result to have errors, but it succeeded"
288
+ end
289
+ end
290
+
291
+ supports_block_expectations
292
+ end
293
+
294
+ # Alias for singular form
295
+ RSpec::Matchers.alias_matcher :have_graphql_error, :have_graphql_errors
296
+
297
+ # Matcher for checking if GraphQL query succeeds
298
+ #
299
+ # @example
300
+ # expect(result).to succeed_graphql
301
+ #
302
+ RSpec::Matchers.define :succeed_graphql do
303
+ match do |result|
304
+ result["errors"].nil? || result["errors"].empty?
305
+ end
306
+
307
+ failure_message do |result|
308
+ "expected GraphQL query to succeed, but got errors: #{result["errors"]}"
309
+ end
310
+ end
311
+
312
+ # Matcher for checking if GraphQL result has a specific field
313
+ #
314
+ # @example
315
+ # expect(result.data).to have_graphql_field("id")
316
+ # expect(result.data).to have_graphql_field(:name)
317
+ #
318
+ RSpec::Matchers.define :have_graphql_field do |field_name|
319
+ match do |data|
320
+ data.is_a?(Hash) && data.key?(field_name.to_s)
321
+ end
322
+
323
+ failure_message do |data|
324
+ "expected data to have field #{field_name.inspect}, but data was: #{data.inspect}"
325
+ end
326
+ end
327
+
328
+ # Matcher for checking if GraphQL result has multiple fields with specific values
329
+ #
330
+ # @example
331
+ # expect(result.data).to have_graphql_fields(id: "123", name: "John")
332
+ #
333
+ RSpec::Matchers.define :have_graphql_fields do |expected_fields|
334
+ match do |data|
335
+ return false unless data.is_a?(Hash)
336
+
337
+ expected_fields.all? do |field, value|
338
+ data.key?(field.to_s) && data[field.to_s] == value
339
+ end
340
+ end
341
+
342
+ failure_message do |data|
343
+ missing_or_wrong = expected_fields.reject do |field, value|
344
+ data.key?(field.to_s) && data[field.to_s] == value
345
+ end
346
+
347
+ "expected data to have fields #{expected_fields.inspect}, " \
348
+ "but #{missing_or_wrong.inspect} did not match. " \
349
+ "Data was: #{data.inspect}"
350
+ end
351
+ end
352
+ end
353
+ end
354
+ end
355
+
356
+ RSpec.configure do |config|
357
+ config.include Zenspec::Matchers::GraphQLMatchers
358
+ end