graphql-autotest 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []