graphql-autotest 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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bdebda4eb82e44a457ec65e9384f1a338311ef74516511f73ec20b4d2cd2e1cc
4
+ data.tar.gz: 9796606bbf638c223082b7dcb10179cca31f30c19d0e3ffd028ebda1c1250903
5
+ SHA512:
6
+ metadata.gz: f0de4ae0c8ad47fd05fb75243b95e64e8f8481ff506154d28003e4d2e701428f7151364f768cb6198f2f4a91755b3f11cbcc04ce71937eed2baffe544c2516b7
7
+ data.tar.gz: fa0ecb913081ac8e642eb83aeb4b6b5ec14baf8c0af8052a74e427407d1970d4056b4fea3f9a50caca9c67b853d40d70e3a7c030a7ec66599be3715b2d8d9216
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /Gemfile.lock
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.7.0
4
+ - 2.6.5
5
+ - ruby-head
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in graphql-autotest.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem 'minitest', '>= 5'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Bit Journey, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,132 @@
1
+ # GraphQL::Autotest
2
+
3
+ GraphQL::Autotest tests your GraphQL API with auto-generated queries.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'graphql-autotest'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install graphql-autotest
20
+
21
+ ## Usage
22
+
23
+ ### Generate queries and execute them
24
+
25
+ ```ruby
26
+ require 'graphql/autotest'
27
+
28
+ class YourSchema < GraphQL::Schema
29
+ end
30
+
31
+ runner = GraphQL::Autotest::Runner.new(
32
+ schema: YourSchema,
33
+ # The context that is passed to GraphQL::Schema.execute
34
+ context: { current_user: User.first },
35
+ )
36
+
37
+ # * Generate queries from YourSchema
38
+ # * Then execute the queries
39
+ # * Raise an error if the results contain error(s)
40
+ runner.report!
41
+ ```
42
+
43
+ ### Generate queries from `GraphQL::Schema` (not execute)
44
+
45
+ ```ruby
46
+ require 'graphql/autotest'
47
+
48
+ class YourSchema < GraphQL::Schema
49
+ end
50
+
51
+ fields = GraphQL::Autotest::QueryGenerator.generate(document: YourSchema.to_document)
52
+
53
+ # Print all generated queries
54
+ fields.each do |field|
55
+ puts field.to_query
56
+ end
57
+ ```
58
+
59
+ ### Generate queries from file (not execute)
60
+
61
+ It is useful for non graphql-ruby user.
62
+
63
+ ```ruby
64
+ require 'graphql/autotest'
65
+
66
+ fields = GraphQL::Autotest::QueryGenerator.from_file(path: 'path/to/definition.graphql')
67
+
68
+ # Print all generated queries
69
+ fields.each do |field|
70
+ puts field.to_query
71
+ end
72
+ ```
73
+
74
+ ### Configuration
75
+
76
+ `GraphQL::Autotest::Runner.new`, `GraphQL::Autotest::QueryGenerator.generate` and `GraphQL::Autotest::QueryGenerator.from_file` receives the following arguments to configure how to generates queries.
77
+
78
+ * `arguments_fetcher`
79
+ * A proc to fill arguments of the received field.
80
+ * default: `GraphQL::Autotest::ArgumentsFetcher::DEFAULT`, that allows empty arguments, and arguments that has no required argument.
81
+ * You need to specify the proc if you need to test field that has required arguments.
82
+ * `max_depth`
83
+ * Max query depth.
84
+ * default: 10
85
+ * `skip_if`
86
+ * A proc to specify field that you'd like to skip to generate query.
87
+ * default: skip nothing
88
+
89
+ For example:
90
+
91
+ ```ruby
92
+ require 'graphql/autotest'
93
+
94
+ class YourSchema < GraphQL::Schema
95
+ end
96
+
97
+ # Fill `first` argument to reduce result size.
98
+ fill_first = proc do |field|
99
+ field.arguments.any? { |arg| arg.name == 'first' } && { first: 5 }
100
+ end
101
+
102
+ # Skip a sensitive field
103
+ skip_if = proc do |field|
104
+ field.name == 'sensitiveField'
105
+ end
106
+
107
+ fields = GraphQL::Autotest::QueryGenerator.generate(
108
+ document: YourSchema.to_document,
109
+ arguments_fetcher: GraphQL::Autotest::ArgumentsFetcher.combine(
110
+ fill_first,
111
+ GraphQL::Autotest::ArgumentsFetcher::DEFAULT,
112
+ ),
113
+ max_depth: 5,
114
+ skip_if: skip_if,
115
+ )
116
+
117
+ # Print all generated queries
118
+ fields.each do |field|
119
+ puts field.to_query
120
+ end
121
+ ```
122
+
123
+ ## Development
124
+
125
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
126
+
127
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
128
+
129
+ ## Contributing
130
+
131
+ Bug reports and pull requests are welcome on GitHub at https://github.com/bitjourney/graphql-autotest.
132
+
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+ task :default => :test
4
+
5
+ Rake::TestTask.new do |test|
6
+ test.libs << 'test'
7
+ test.test_files = Dir['test/**/*_test.rb']
8
+ test.verbose = true
9
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "graphql/autotest"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,29 @@
1
+ require_relative 'lib/graphql/autotest/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "graphql-autotest"
5
+ spec.version = GraphQL::Autotest::VERSION
6
+ spec.authors = ["Masataka Pocke Kuwabara"]
7
+ spec.email = ["kuwabara@pocke.me"]
8
+
9
+ spec.summary = %q{Test GraphQL queries automatically}
10
+ spec.description = %q{Test GraphQL queries automatically}
11
+ spec.homepage = "https://github.com/bitjourney/graphql-autotest"
12
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
13
+ spec.license = 'MIT'
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = spec.homepage
17
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ end
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_runtime_dependency 'graphql'
29
+ end
@@ -0,0 +1,14 @@
1
+ require 'graphql'
2
+
3
+ require_relative "autotest/version"
4
+ require_relative "autotest/util"
5
+ require_relative 'autotest/field'
6
+ require_relative 'autotest/arguments_fetcher'
7
+ require_relative 'autotest/report'
8
+ require_relative 'autotest/runner'
9
+ require_relative "autotest/query_generator"
10
+
11
+ module GraphQL
12
+ module Autotest
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ module GraphQL
2
+ module Autotest
3
+ module ArgumentsFetcher
4
+ def self.combine(*strategy)
5
+ -> (*args, **kwargs) do
6
+ strategy.find do |s|
7
+ r = s.call(*args, **kwargs)
8
+ break r if r
9
+ end
10
+ end
11
+ end
12
+
13
+ EMPTY = -> (field, ancestors:) { field.arguments.empty? && {} }
14
+ NO_REQUIRED = -> (field, ancestors:) { field.arguments.none? { |arg| Util.non_null?(arg.type) } && {} }
15
+ DEFAULT = combine(EMPTY, NO_REQUIRED)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,58 @@
1
+ module GraphQL
2
+ module Autotest
3
+ class Field < Struct.new(:name, :children, :arguments, keyword_init: true)
4
+ TYPE_NAME = Field.new(name: '__typename', children: nil)
5
+
6
+ def to_query
7
+ return name unless children
8
+
9
+ <<~GRAPHQL
10
+ #{name}#{arguments_to_query} {
11
+ #{indent(children_to_query, 2)}
12
+ }
13
+ GRAPHQL
14
+ end
15
+
16
+ private def children_to_query
17
+ sorted_children.map do |child|
18
+ child.to_query
19
+ end.join("\n")
20
+ end
21
+
22
+ private def arguments_to_query
23
+ return unless arguments
24
+ return if arguments.empty?
25
+
26
+ inner = arguments.map do |k, v|
27
+ "#{k}: #{v}"
28
+ end.join(', ')
29
+ "(#{inner})"
30
+ end
31
+
32
+ private def indent(str, n)
33
+ str.lines(chomp: true).map do |line|
34
+ if line.empty?
35
+ ""
36
+ else
37
+ " " * n + line
38
+ end
39
+ end.join("\n")
40
+ end
41
+
42
+ private def sorted_children
43
+ children.sort_by { |child| child.sort_key }
44
+ end
45
+
46
+ protected def sort_key
47
+ [
48
+ # '__typename' is at the last
49
+ name == '__typename' ? 1 : 0,
50
+ # no-children field is at the first
51
+ children ? 1 : 0,
52
+ # alphabetical order
53
+ name,
54
+ ]
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,77 @@
1
+ module GraphQL
2
+ module Autotest
3
+ class QueryGenerator
4
+ attr_reader :document, :arguments_fetcher, :max_depth, :skip_if
5
+ private :document, :arguments_fetcher, :max_depth, :skip_if
6
+
7
+ def self.from_file(path: nil, content: nil, **kw)
8
+ raise ArgumentError, "path or content is required" if !path && !content
9
+
10
+ content ||= File.read(path)
11
+ document = GraphQL.parse(content)
12
+ generate(document: document, **kw)
13
+ end
14
+
15
+ # See Runner#initialize for arguments documentation.
16
+ def self.generate(document:, arguments_fetcher: ArgumentsFetcher::DEFAULT, max_depth: 10, skip_if: -> (_field, **) { false })
17
+ self.new(document: document, arguments_fetcher: arguments_fetcher, max_depth: max_depth, skip_if: skip_if).generate
18
+ end
19
+
20
+ def initialize(document:, arguments_fetcher:, max_depth: , skip_if:)
21
+ @document = document
22
+ @arguments_fetcher = arguments_fetcher
23
+ @max_depth = max_depth
24
+ @skip_if = skip_if
25
+ end
26
+
27
+ def generate
28
+ query_type = type_definition('Query')
29
+ testable_fields(query_type)
30
+ end
31
+
32
+ # It returns testable fields as a tree.
33
+ # "Testable" means that it can fill the arguments.
34
+ private def testable_fields(type_def, called_fields: Set.new, depth: 0, ancestors: [])
35
+ return [Field::TYPE_NAME] if depth > max_depth
36
+
37
+ type_def.fields.map do |f|
38
+ next if skip_if.call(f, ancestors: ancestors)
39
+
40
+ arguments = arguments_fetcher.call(f, ancestors: ancestors)
41
+ next unless arguments
42
+ already_called_key = [type_def, f.name, ancestors.first&.name]
43
+ next if called_fields.include?(already_called_key) && f.name != 'id'
44
+
45
+ called_fields << already_called_key
46
+
47
+ field_type = Util.unwrap f.type
48
+ field_type_def = type_definition(field_type.name)
49
+
50
+ case field_type_def
51
+ when nil, GraphQL::Language::Nodes::EnumTypeDefinition, GraphQL::Language::Nodes::ScalarTypeDefinition
52
+ Field.new(name: f.name, children: nil, arguments: arguments)
53
+ when GraphQL::Language::Nodes::UnionTypeDefinition
54
+ possible_types = field_type_def.types.map do |t|
55
+ t_def = type_definition(t.name)
56
+ children = testable_fields(t_def, called_fields: called_fields.dup, depth: depth + 1, ancestors: [f, *ancestors])
57
+ Field.new(name: "... on #{t.name}", children: children)
58
+ end
59
+ Field.new(name: f.name, children: possible_types + [Field::TYPE_NAME], arguments: arguments)
60
+ else
61
+ children = testable_fields(field_type_def, called_fields: called_fields.dup, depth: depth + 1, ancestors: [f, *ancestors])
62
+
63
+ Field.new(
64
+ name: f.name,
65
+ children: children,
66
+ arguments: arguments,
67
+ )
68
+ end
69
+ end.compact + [Field::TYPE_NAME]
70
+ end
71
+
72
+ private def type_definition(name)
73
+ document.definitions.find { |f| f.name == name }
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,27 @@
1
+ module GraphQL
2
+ module Autotest
3
+ class Report < Struct.new(:executions, keyword_init: true)
4
+ Execution = Struct.new(:query, :result, keyword_init: true) do
5
+ def query_summary
6
+ query.lines[1].strip
7
+ end
8
+
9
+ def to_error_message
10
+ query_summary + "\n" + result['errors'].inspect
11
+ end
12
+ end
13
+
14
+ def error?
15
+ !errored_executions.empty?
16
+ end
17
+
18
+ def errored_executions
19
+ executions.select { |e| e.result['errors'] }
20
+ end
21
+
22
+ def raise_if_error!
23
+ raise errored_executions.map(&:to_error_message).join("\n\n") if error?
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,60 @@
1
+ module GraphQL
2
+ module Autotest
3
+ class Runner
4
+ attr_reader :schema, :context, :arguments_fetcher, :max_depth, :skip_if
5
+ private :schema, :context, :arguments_fetcher, :max_depth, :skip_if
6
+
7
+ # @param schema [Class<GraphQL::Schema>]
8
+ # @param context [Hash] it passes to GraphQL::Schema.execute
9
+ # @param arguments_fetcher [Proc] A proc receives a field and ancestors keyword argument, and it returns a Hash. The hash is passed to call the field.
10
+ # @param max_depth [Integer] Max query depth. It is recommended to specify to avoid too large query.
11
+ # @param skip_if [Proc] A proc receives a field and ancestors keyword argument, and it returns a boolean. If it returns ture, the field is skipped.
12
+ def initialize(schema:, context:, arguments_fetcher: ArgumentsFetcher::DEFAULT, max_depth: 10, skip_if: -> (_field, **) { false })
13
+ @schema = schema
14
+ @context = context
15
+ @arguments_fetcher = arguments_fetcher
16
+ @max_depth = max_depth
17
+ @skip_if = skip_if
18
+ end
19
+
20
+ def report(dry_run: false)
21
+ report = Report.new(executions: [])
22
+
23
+ fields = QueryGenerator.generate(
24
+ document: schema.to_document,
25
+ arguments_fetcher: arguments_fetcher,
26
+ max_depth: max_depth,
27
+ skip_if: skip_if,
28
+ )
29
+ fields.each do |f|
30
+ q = f.to_query
31
+ q = <<~GRAPHQL
32
+ {
33
+ #{q.indent(2)}
34
+ }
35
+ GRAPHQL
36
+
37
+ result = if dry_run
38
+ {}
39
+ else
40
+ schema.execute(
41
+ document: GraphQL.parse(q),
42
+ variables: {},
43
+ operation_name: nil,
44
+ context: context,
45
+ )
46
+ end
47
+ report.executions << Report::Execution.new(query: q, result: result)
48
+ end
49
+
50
+ report
51
+ end
52
+
53
+ def report!(dry_run: false)
54
+ report(dry_run: dry_run).tap do |r|
55
+ r.raise_if_error!
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,17 @@
1
+ module GraphQL
2
+ module Autotest
3
+ module Util
4
+ extend self
5
+
6
+ def non_null?(type)
7
+ type.is_a?(GraphQL::Language::Nodes::NonNullType)
8
+ end
9
+
10
+ def unwrap(type)
11
+ return type unless type.respond_to?(:of_type)
12
+
13
+ unwrap(type.of_type)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ module GraphQL
2
+ module Autotest
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graphql-autotest
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Masataka Pocke Kuwabara
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-03-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: graphql
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Test GraphQL queries automatically
28
+ email:
29
+ - kuwabara@pocke.me
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".gitignore"
35
+ - ".travis.yml"
36
+ - Gemfile
37
+ - LICENSE
38
+ - README.md
39
+ - Rakefile
40
+ - bin/console
41
+ - bin/setup
42
+ - graphql-autotest.gemspec
43
+ - lib/graphql/autotest.rb
44
+ - lib/graphql/autotest/arguments_fetcher.rb
45
+ - lib/graphql/autotest/field.rb
46
+ - lib/graphql/autotest/query_generator.rb
47
+ - lib/graphql/autotest/report.rb
48
+ - lib/graphql/autotest/runner.rb
49
+ - lib/graphql/autotest/util.rb
50
+ - lib/graphql/autotest/version.rb
51
+ homepage: https://github.com/bitjourney/graphql-autotest
52
+ licenses:
53
+ - MIT
54
+ metadata:
55
+ homepage_uri: https://github.com/bitjourney/graphql-autotest
56
+ source_code_uri: https://github.com/bitjourney/graphql-autotest
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: 2.3.0
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 3.2.0.pre1
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: Test GraphQL queries automatically
76
+ test_files: []