Repository: khamusa/rspec-graphql_matchers Branch: master Commit: 97324d257f28 Files: 40 Total size: 61.6 KB Directory structure: gitextract_0bqch8nx/ ├── .codeclimate.yml ├── .editorconfig ├── .github/ │ └── workflows/ │ └── rspec.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin/ │ ├── console │ └── setup ├── lib/ │ └── rspec/ │ ├── graphql_matchers/ │ │ ├── accept_argument.rb │ │ ├── accept_arguments.rb │ │ ├── base_matcher.rb │ │ ├── be_of_type.rb │ │ ├── have_a_field.rb │ │ ├── have_a_field_matchers/ │ │ │ ├── of_type.rb │ │ │ ├── with_deprecation_reason.rb │ │ │ ├── with_hash_key.rb │ │ │ ├── with_metadata.rb │ │ │ └── with_property.rb │ │ ├── implement.rb │ │ ├── matchers.rb │ │ ├── types_helper.rb │ │ └── version.rb │ └── graphql_matchers.rb ├── rspec-graphql_matchers.gemspec └── spec/ ├── rspec/ │ ├── accept_argument_matcher_spec.rb │ ├── accept_arguments_matcher_spec.rb │ ├── be_of_type_matcher_spec.rb │ ├── graphql_matchers_spec.rb │ ├── have_a_field_matcher_spec.rb │ ├── have_a_return_field_spec.rb │ ├── have_an_input_field_matcher_spec.rb │ ├── implement_matcher_spec.rb │ └── readme_spec.rb └── spec_helper.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codeclimate.yml ================================================ --- engines: duplication: enabled: true config: languages: - ruby - javascript - python - php fixme: enabled: true rubocop: enabled: true ratings: paths: - "**.inc" - "**.js" - "**.jsx" - "**.module" - "**.php" - "**.py" - "**.rb" exclude_paths: - Gemfile.lock - spec/ - rspec-graphql_matchers.gemspec - bin/ - Rakefile ================================================ FILE: .editorconfig ================================================ # EditorConfig help us maintain consistent coding style between different editors. # # EditorConfig # http://editorconfig.org # root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false indent_size = 4 ================================================ FILE: .github/workflows/rspec.yml ================================================ name: RSpec # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch on: [push, pull_request] jobs: # This workflow contains a single job "rspec" rspec: # The type of runner that the job will run on runs-on: ubuntu-latest strategy: matrix: ruby_version: [2.6, 2.7, '3.0', 3.1, 3.2] # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 - name: Set up Ruby ${{ matrix.ruby_version }} uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby_version }} bundler-cache: true - name: Run RSpec run: bundle exec rspec ================================================ FILE: .gitignore ================================================ /.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ ================================================ FILE: .rspec ================================================ --format documentation --color ================================================ FILE: .rubocop.yml ================================================ AllCops: DisplayCopNames: true DisplayStyleGuide: true Layout/AlignParameters: EnforcedStyle: with_fixed_indentation Metrics/LineLength: Max: 90 Layout/MultilineMethodCallIndentation: EnforcedStyle: indented Layout/SpaceBeforeFirstArg: Include: - db/migrate/*.rb Enabled: false Style/Documentation: Enabled: false Style/ClassAndModuleChildren: Enabled: true Style/Lambda: Enabled: false Metrics/MethodLength: Max: 15 Metrics/BlockLength: Exclude: - spec/rspec/*.rb Metrics/ModuleLength: Exclude: - spec/rspec/*.rb ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## 2.0.0 (April 16th, 2023) - Adds compatibility with graphql-ruby 2.0+. If you're still using an earlier version, you should stick to 1.4.x. ### Deprecations - The usage of the `#types` helper when writing tests (if you're not including `RSpec::GraphqlMatchers::TypesHelper` anywhere, you shoudn't have to change anything). Use `GraphQL::Types::` instead - The usage of type instances as expected value for a type comparison is deprecated. Instead, use the string that represents the type specification. Examples: ``` # Bad - These are deprecated: expect(a_type).to have_a_field(:lorem).of_type(GraphQL::Types::ID) expect(a_type).to have_a_field(:lorem).of_type(Types::MyCustomType) # Good - Use these instead expect(a_type).to have_a_field(:lorem).of_type('ID') expect(a_type).to have_a_field(:lorem).of_type('MyCustomType') ``` The reason behind this change relies on the fact that we should have a decoupling between the internal constants used to define our API and the public names exposed through it. If we test a published API using the internal constants, it is possible to perform breaking changes by renaming the graphql names of our types or entities without actually breaking the tests. If we're performing a breaking change to a public API, we want our tests to fail. ## 1.4.0 (April 16th, 2023) - Removal of deprecated calls to #to_graphql, replacing them with #to_type_signature that was added in graphql-ruby 1.8.3 (https://github.com/khamusa/rspec-graphql_matchers/pull/43 @RobinDaugherty) - Dropped support for legacy .define API - Dropped support to old relay interfaces - Fixed with_hash_key matcher for newer versions of graphql-ruby - Fixed errors that occured in some edge cases when generating descriptions for accept_argument and have_a_field matchers - Documentations improvement and general cleanup - Dropped support to graphql-ruby versions before 1.10.12. ## 1.3.1 (Aug 2nd, 2021) - Corrected gem dependencies so it properly requires RSpec when loaded (thanks to @itay-grudev) ## 1.3.0 (May 7th, 2020) - `accept_argument` matcher accepts underscored argument names and passes even if the actual argument is camel-cased (https://github.com/khamusa/rspec-graphql_matchers/pull/32 thanks to @TonyArra); - `have_a_field` matcher accepts `.with_deprecation_reason` (https://github.com/khamusa/rspec-graphql_matchers/pull/34 thanks to @TonyArra). ## 1.2.1 (March 31st, 2020) - Fixed issue causing the last release to break expectations against snake_cased fields (fixes https://github.com/khamusa/rspec-graphql_matchers/issues/30). ## 1.2 (Feb 6th, 2020) - Added support to underscored arguments in have_field (https://github.com/khamusa/rspec-graphql_matchers/pull/29 thanks to @makketagg) ## 1.1 (Sep 19th, 2019) - Added graphql-ruby 1.9.x support (thanks to @severin) ## 1.0.1 (June 22th, 2019) ### Bug fixes - Fixed issue causing `have_a_field(x).of_type(Y)` to fail on fields defined on implemented interfaces that were defined with legacy syntax. ## 1.0 (June, 2019) ### Breaking changes - Support to property and hash_key matchers will be dropped on upcoming releases. ### Deprecations - `.with_metadata` and `.with_property` matchers for fields will be removed on the next release; - `.accept_arguments` (plural form) will be removed on the next release; - `.accept_argument` (singular form) receiving a hash with a single or multiple arguments will no longer be supported, use `accept_argument(name).of_type('MyType')` instead. ### New features - Add support for Class-based type definition api (adds support for graphql-ruby v1.8.x). Please note that `.with_metadata` and `.with_property` support has been kept but will only work on fields defined using the legacy api. - Implemented `accept_argument(arg_name).of_type('MyType')´ matcher, which returns much clearer error messages when the arguments are not found on the target type. ### Bug fixes ## 0.7.1 (July 27, 2017) Changelog fixes. ## 0.7.0 (July 27, 2017) ### New features - (#3, #8) New chainable matchers `with_property`, `with_hash_key` and `with_metadata` (Thanks to @marcgreenstock). ### Improvements - Default Raketask runs rubocop specs as well (Thanks to @marcgreenstock). ## 0.6.0 (July 25, 2017) ### New features - (PR #6) New matchers for interfaces: `expect(...).to implement(interface)` (Thanks to @marcgreenstock). ## 0.5.0 (May 10, 2017) ### New features - (PR #4) New matchers for mutations: `have_an_input_field` and `have_a_return_field` (Thanks to @aaronklaassen). ## 0.4.0 (Feb, 2017) ### New features - Improvements on error messages of have_a_field(...).of_type(...) ### Bug fixes - Fixed a bug preventing proper type checking when using matchers in the form have_a_field(fname).of_type(types.X). The bug would not happen when providing the type expectation as a string, only when using the GraphQL constants. ## 0.3.0 (Sep 16, 2016) ### Breaking changes ### Deprecations ### New features - New matcher have_field(field_name).of_type(type) for testing types and their fields ### Bug fixes ## 0.2.0 Initial public release ### Breaking changes ### Deprecations ### New features - New matcher be_of_type for testing fields - New matcher accept_arguments for testing fields ### Bug fixes ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Code of Conduct As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery * Personal attacks * Trolling or insulting/derogatory comments * Public or private harassment * Publishing other's private information, such as physical or electronic addresses, without explicit permission * Other unethical or unprofessional conduct Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at gb.samuel@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.3.0, available at [http://contributor-covenant.org/version/1/3/0/][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/3/0/ ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source 'https://rubygems.org' # Specify your gem's dependencies in rspec-graphql_matchers.gemspec gemspec ================================================ FILE: LICENSE.txt ================================================ The MIT License (MIT) Copyright (c) 2016 Samuel Brandão Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![RSpec Status](https://github.com/khamusa/rspec-graphql_matchers/actions/workflows/rspec.yml/badge.svg) # Rspec::GraphqlMatchers Convenient rspec matchers for testing your [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) API/Schema. ## Installation ``` gem 'rspec-graphql_matchers' ``` ## Usage The matchers currently supported are: - `expect(a_graphql_object).to have_a_field(field_name).of_type(valid_type)` - `expect(a_graphql_object).to implement(interface_name, ...)` - `expect(a_mutation_type).to have_a_return_field(field_name).returning(valid_type)` - `expect(a_mutation_type).to have_an_input_field(field_name).of_type(valid_type)` - `expect(a_field).to be_of_type(valid_type)` - `expect(an_input).to accept_argument(argument_name).of_type(valid_type)` Where `valid_type` is a your type signature as a String: `"String!"`, `"Int!"`, `"[String]!"` (note the exclamation mark at the end, as required by the [GraphQL specifications](http://graphql.org/). Please note that using references to type instances is deprecated and will be removed in a future release. ## Examples Given a GraphQL object defined as ```ruby class PostType < GraphQL::Schema::Object graphql_name "Post" description "A blog post" implements GraphQL::Types::Relay::Node field :id, ID, null: false field :comments, [String], null: false field :isPublished, Boolean, null: true field :published, Boolean, null: false, deprecation_reason: 'Use isPublished instead' field :subposts, PostType, null: true do argument :filter, String, required: false argument :id, ID, required: false argument :isPublished, Boolean, required: false end end ``` ### 1) Test your type defines the correct fields: ```ruby describe PostType do subject { described_class } it { is_expected.to have_field(:id).of_type("ID!") } it { is_expected.to have_field(:comments).of_type("[String!]!") } it { is_expected.to have_field(:isPublished).of_type("Boolean") } # Check a field is deprecated it { is_expected.to have_field(:published).with_deprecation_reason } it { is_expected.to have_field(:published).with_deprecation_reason('Use isPublished instead') } it { is_expected.not_to have_field(:published).with_deprecation_reason('Wrong reason') } it { is_expected.not_to have_field(:isPublished).with_deprecation_reason } # The gem automatically converts field names to CamelCase, so this will # pass even though the field was defined as field :isPublished it { is_expected.to have_field(:is_published).of_type("Boolean") } end ``` ### 2) Test a specific field type with `be_of_type` matcher: ```ruby describe PostType do describe 'id' do subject { PostType.fields['id'] } it { is_expected.to be_of_type('ID!') } it { is_expected.not_to be_of_type('Float!') } end describe 'subposts' do subject { PostType.fields['subposts'] } it { is_expected.to be_of_type('Post') } end end ``` ### 3) Test the arguments accepted by a field with `accept_argument` matcher: ```ruby describe PostType do describe 'subposts' do subject { PostType.fields['subposts'] } it 'accepts a filter and an id argument, of types String and ID' do expect(subject).to accept_argument(:filter).of_type('String') expect(subject).to accept_argument(:id).of_type('ID') end it { is_expected.not_to accept_argument(:weirdo) } # The gem automatically converts argument names to CamelCase, so this will # pass even though the argument was defined as :isPublished it { is_expected.to accept_argument(:is_published).of_type("Boolean") } end end ``` ### 4) Test an object's interface implementations: ```ruby describe PostType do subject { described_class } it 'implements interface Node' do expect(subject).to implement('Node') end it { is_expected.not_to implement('OtherInterface') } end ``` ### 5) Using camelize: false on field names By default the graphql gem camelizes field names. This gem deals with it transparently: ```ruby class ObjectMessingWithCamelsAndSnakesType < GraphQL::Schema::Object graphql_name 'ObjectMessingWithCamelsAndSnakes' implements GraphQL::Types::Relay::Node field :me_gusta_los_camellos, ID, null: false # note the camelize: false field :prefiero_serpientes, ID, null: false, camelize: false end ``` The following specs demonstrate the current behavior of the gem regarding fields: ```ruby describe ObjectMessingWithCamelsAndSnakesType do subject { described_class } # For a field name that was automatically camelized, you can add expectations # against both versions and we handle it transparently: it { is_expected.to have_a_field(:meGustaLosCamellos) } it { is_expected.to have_a_field(:me_gusta_los_camellos) } # However, when using camelize: false, you have to use the exact case of the field definition: it { is_expected.to have_a_field(:prefiero_serpientes) } it { is_expected.not_to have_a_field(:prefieroSerpientes) } # Note we're using `not_to` end ``` This behaviour is currently active only on field name matching. PRs are welcome to reproduce it to arguments as well. ## TODO - New matchers! ## Contributing - Send Bug reports, suggestions or any general question through the [Issue tracker](https://github.com/khamusa/rspec-graphql_matchers/issues). Think of another matcher that could be useful? This is the place to ask, or... - Pull requests are welcome through the usual procedure: fork the project, commit your changes and open the [PR](https://github.com/khamusa/rspec-graphql_matchers/pulls). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). ================================================ FILE: Rakefile ================================================ # frozen_string_literal: true require 'bundler/gem_tasks' require 'rspec/core/rake_task' require 'rubocop/rake_task' RSpec::Core::RakeTask.new(:spec) RuboCop::RakeTask.new task default: %i[spec rubocop] ================================================ FILE: bin/console ================================================ #!/usr/bin/env ruby # frozen_string_literal: true require 'bundler/setup' require 'rspec/graphql_matchers' # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require 'irb' IRB.start ================================================ FILE: bin/setup ================================================ #!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here ================================================ FILE: lib/rspec/graphql_matchers/accept_argument.rb ================================================ # frozen_string_literal: true require_relative 'base_matcher' require_relative './have_a_field_matchers/of_type' module RSpec module GraphqlMatchers class AcceptArgument < BaseMatcher def initialize(expected_arg_name) @expectations = [] if expected_arg_name.is_a?(Hash) (expected_arg_name, expected_type) = expected_arg_name.to_a.first of_type(expected_type) warn 'DEPRECATION WARNING: using accept_arguments with a hash will be '\ 'deprecated on the next major release. Use the format ' \ "`accept_argument(:argument_name).of_type('ExpectedType!') instead.`" end @expected_arg_name = expected_arg_name.to_s @expected_camel_arg_name = GraphQL::Schema::Member::BuildType.camelize( @expected_arg_name ) end def matches?(graph_object) @graph_object = graph_object @actual_argument = field_arguments[@expected_arg_name] @actual_argument ||= field_arguments[@expected_camel_arg_name] return false if @actual_argument.nil? @results = @expectations.reject do |matcher| matcher.matches?(@actual_argument) end @results.empty? end def of_type(expected_field_type) @expectations << HaveAFieldMatchers::OfType.new(expected_field_type) self end def failure_message base_msg = "expected #{member_name(@graph_object)} " \ "to accept argument `#{@expected_arg_name}`" \ return "#{base_msg} #{failure_messages.join(', ')}" if @actual_argument "#{base_msg} but no argument was found with that name" end def description ["accept argument `#{@expected_arg_name}`"].concat(descriptions).join(', ') end private def descriptions results.map(&:description) end def failure_messages results.map(&:failure_message) end def field_arguments if @graph_object.respond_to?(:arguments) @graph_object.public_send(:arguments) else raise "Invalid object #{@graph_object} provided to accept_argument " \ 'matcher. It does not seem to be a valid GraphQL object type.' end end def results @results ||= [] end end end end ================================================ FILE: lib/rspec/graphql_matchers/accept_arguments.rb ================================================ # frozen_string_literal: true require_relative 'base_matcher' module RSpec module GraphqlMatchers class AcceptArguments < BaseMatcher attr_reader :actual_field, :expected_args def initialize(expected_args) @expected_args = expected_args end def matches?(actual_field) @actual_field = actual_field @expected_args.all? do |arg_name, arg_type| matches_argument?(arg_name, arg_type) end end def failure_message "expected field '#{member_name(actual_field)}' to accept arguments "\ "#{describe_arguments(expected_args)}" end def description "accept arguments #{describe_arguments(expected_args)}" end private def matches_argument?(arg_name, arg_type) camel_arg_name = GraphQL::Schema::Member::BuildType.camelize(arg_name.to_s) actual_arg = actual_field.arguments[arg_name.to_s] actual_arg ||= actual_field.arguments[camel_arg_name] actual_arg && types_match?(actual_arg.type, arg_type) end def describe_arguments(what_args) what_args.sort.map do |arg_name, arg_type| "#{arg_name}(#{type_name(arg_type)})" end.join(', ') end end end end ================================================ FILE: lib/rspec/graphql_matchers/base_matcher.rb ================================================ # frozen_string_literal: true module RSpec module GraphqlMatchers class BaseMatcher private def member_name(member) member.respond_to?(:graphql_name) && member.graphql_name || member.respond_to?(:name) && member.name || member.inspect end def types_match?(actual_type, expected_type) expected_type.nil? || type_name(expected_type) == type_name(actual_type) end def type_name(a_type) a_type = a_type.to_type_signature if a_type.respond_to?(:to_type_signature) a_type.to_s end end end end ================================================ FILE: lib/rspec/graphql_matchers/be_of_type.rb ================================================ # frozen_string_literal: true require_relative 'base_matcher' module RSpec module GraphqlMatchers class BeOfType < BaseMatcher attr_reader :sample, :expected def initialize(expected) @expected = expected end def matches?(actual_sample) @sample = to_graphql(actual_sample) sample.respond_to?(:type) && types_match?(sample.type, @expected) end def failure_message "expected field '#{member_name(sample)}' to " \ "be of type '#{type_name(expected)}', " \ "but it was '#{type_name(sample.type)}'" end def description "be of type '#{expected}'" end private def to_graphql(field_sample) return field_sample unless field_sample.respond_to?(:to_type_signature) field_sample.to_type_signature end end end end ================================================ FILE: lib/rspec/graphql_matchers/have_a_field.rb ================================================ # frozen_string_literal: true require_relative 'base_matcher' require_relative './have_a_field_matchers/of_type' require_relative './have_a_field_matchers/with_property' require_relative './have_a_field_matchers/with_metadata' require_relative './have_a_field_matchers/with_hash_key' require_relative './have_a_field_matchers/with_deprecation_reason' module RSpec module GraphqlMatchers class HaveAField < BaseMatcher def initialize(expected_field_name, fields = :fields) @expected_field_name = expected_field_name.to_s @expected_camel_field_name = GraphQL::Schema::Member::BuildType.camelize( @expected_field_name ) @fields = fields.to_sym @expectations = [] end def matches?(graph_object) @graph_object = graph_object return false if actual_field.nil? @results = @expectations.reject do |matcher| matcher.matches?(actual_field) end @results.empty? end def that_returns(expected_field_type) @expectations << HaveAFieldMatchers::OfType.new(expected_field_type) self end alias returning that_returns alias of_type that_returns def with_property(expected_property_name) @expectations << HaveAFieldMatchers::WithProperty.new(expected_property_name) self end def with_hash_key(expected_hash_key) @expectations << HaveAFieldMatchers::WithHashKey.new(expected_hash_key) self end def with_metadata(expected_metadata) @expectations << HaveAFieldMatchers::WithMetadata.new(expected_metadata) self end def with_deprecation_reason(expected_reason = nil) @expectations << HaveAFieldMatchers::WithDeprecationReason.new(expected_reason) self end def failure_message base_msg = "expected #{member_name(@graph_object)} " \ "to define field `#{@expected_field_name}`" \ return "#{base_msg} #{failure_messages.join(', ')}" if actual_field "#{base_msg} but no field was found with that name" end def description ["define field `#{@expected_field_name}`"].concat(descriptions).join(', ') end private def actual_field @actual_field ||= begin field = field_collection[@expected_field_name] field ||= field_collection[@expected_camel_field_name] field.respond_to?(:to_type_signature) ? field.to_type_signature : field end end def descriptions results.map(&:description) end def failure_messages results.map(&:failure_message) end def field_collection if @graph_object.respond_to?(@fields) @graph_object.public_send(@fields) else raise "Invalid object #{@graph_object} provided to #{matcher_name} " \ 'matcher. It does not seem to be a valid GraphQL object type.' end end def matcher_name case @fields when :fields then 'have_a_field' when :arguments then 'have_an_input_field' end end def results @results ||= [] end end end end ================================================ FILE: lib/rspec/graphql_matchers/have_a_field_matchers/of_type.rb ================================================ # frozen_string_literal: true module RSpec module GraphqlMatchers module HaveAFieldMatchers class OfType < RSpec::GraphqlMatchers::BeOfType def description "of type `#{type_name(expected)}`" end def failure_message "#{description}, but it was `#{type_name(sample.type)}`" end end end end end ================================================ FILE: lib/rspec/graphql_matchers/have_a_field_matchers/with_deprecation_reason.rb ================================================ # frozen_string_literal: true module RSpec module GraphqlMatchers module HaveAFieldMatchers class WithDeprecationReason def initialize(expected_reason) @expected_reason = expected_reason end def matches?(actual_field) @actual_reason = actual_field.deprecation_reason if @expected_reason.nil? !actual_field.deprecation_reason.nil? else actual_field.deprecation_reason == @expected_reason end end def failure_message message = "#{description}, but it was " message + (@actual_reason.nil? ? 'not deprecated' : "`#{@actual_reason}`") end def description if @expected_reason.nil? 'with a deprecation reason' else "with deprecation reason `#{@expected_reason}`" end end end end end end ================================================ FILE: lib/rspec/graphql_matchers/have_a_field_matchers/with_hash_key.rb ================================================ # frozen_string_literal: true module RSpec module GraphqlMatchers module HaveAFieldMatchers class WithHashKey def initialize(expected_hash_key) @expected_hash_key = expected_hash_key end def description "with hash key `#{@expected_hash_key}`" end def matches?(actual_field) @actual_hash_key = get_hash_key(actual_field) @actual_hash_key == @expected_hash_key.to_sym end def failure_message "#{description}, but it was `#{@actual_hash_key}`" end private def get_hash_key(actual_field) if actual_field.respond_to?(:hash_key) return actual_field.hash_key.to_sym if actual_field.hash_key end if actual_field.respond_to?(:metadata) return actual_field.metadata[:type_class].method_sym end actual_field.method_sym end end end end end ================================================ FILE: lib/rspec/graphql_matchers/have_a_field_matchers/with_metadata.rb ================================================ # frozen_string_literal: true module RSpec module GraphqlMatchers module HaveAFieldMatchers class WithMetadata def initialize(expected_metadata) @expected_metadata = expected_metadata end def description "with metadata `#{@expected_metadata}`" end def matches?(actual_field) @actual_metadata = actual_field.metadata actual_field.metadata == @expected_metadata end def failure_message "#{description}, but it was `#{@actual_metadata}`" end end end end end ================================================ FILE: lib/rspec/graphql_matchers/have_a_field_matchers/with_property.rb ================================================ # frozen_string_literal: true module RSpec module GraphqlMatchers module HaveAFieldMatchers class WithProperty def initialize(expected_property_name) @expected_property_name = expected_property_name end def description "resolving with property `#{@expected_property_name}`" end def matches?(actual_field) @actual_property = property(actual_field).to_sym @actual_property == @expected_property_name.to_sym end def failure_message "#{description}, but it was `#{@actual_property}`" end private def property(field) property = field.property property = field.metadata[:type_class].method_sym if property.nil? property end end end end end ================================================ FILE: lib/rspec/graphql_matchers/implement.rb ================================================ # frozen_string_literal: true require_relative 'base_matcher' module RSpec module GraphqlMatchers class Implement < BaseMatcher def initialize(interfaces) @expected = interfaces.map { |interface| interface_name(interface) } end def matches?(graph_object) @graph_object = graph_object @actual = actual @expected.all? { |name| @actual.include?(name) } end def failure_message message = "expected interfaces: #{@expected.join(', ')}\n" message += "actual interfaces: #{@actual.join(', ')}" message end def failure_message_when_negated message = "unexpected interfaces: #{@expected.join(', ')}\n" message += "actual interfaces: #{@actual.join(', ')}" message end def description "implement #{@expected.join(', ')}" end private def actual if @graph_object.respond_to?(:interfaces) return @graph_object.interfaces.map do |interface| interface_name(interface) end end raise "Invalid object #{@graph_object} provided to #{matcher_name} " \ 'matcher. It does not seem to be a valid GraphQL object type.' end def interface_name(interface) return interface.graphql_name if interface.respond_to?(:graphql_name) interface.to_s end end end end ================================================ FILE: lib/rspec/graphql_matchers/matchers.rb ================================================ # frozen_string_literal: true require 'rspec/matchers' require 'rspec/graphql_matchers/be_of_type' require 'rspec/graphql_matchers/accept_arguments' require 'rspec/graphql_matchers/accept_argument' require 'rspec/graphql_matchers/have_a_field' require 'rspec/graphql_matchers/implement' module RSpec module Matchers def be_of_type(expected) RSpec::GraphqlMatchers::BeOfType.new(expected) end def accept_argument(expected_argument) RSpec::GraphqlMatchers::AcceptArgument.new(expected_argument) end def accept_arguments(expected_args) RSpec::GraphqlMatchers::AcceptArguments.new(expected_args) end # rubocop:disable Naming/PredicateName def have_a_field(field_name) RSpec::GraphqlMatchers::HaveAField.new(field_name) end alias have_field have_a_field def have_an_input_field(field_name) RSpec::GraphqlMatchers::HaveAField.new(field_name, :arguments) end alias have_input_field have_an_input_field def have_a_return_field(field_name) RSpec::GraphqlMatchers::HaveAField.new(field_name) end alias have_return_field have_a_return_field # rubocop:enable Naming/PredicateName def implement(*interface_names) RSpec::GraphqlMatchers::Implement.new(interface_names.flatten) end end end ================================================ FILE: lib/rspec/graphql_matchers/types_helper.rb ================================================ # frozen_string_literal: true require 'graphql' module RSpec module GraphqlMatchers module TypesHelper class << self extend Gem::Deprecate GraphQL::Types.constants.each do |constant_name| klass = GraphQL::Types.const_get(constant_name) define_method(constant_name) { klass } deprecate constant_name, "GraphQL::Types::#{constant_name}", 2023, 10 end end def types TypesHelper end end end end ================================================ FILE: lib/rspec/graphql_matchers/version.rb ================================================ # frozen_string_literal: true module Rspec module GraphqlMatchers VERSION = '2.0.0-rc.0'.freeze end end ================================================ FILE: lib/rspec/graphql_matchers.rb ================================================ # frozen_string_literal: true require 'rspec/graphql_matchers/version' require 'rspec/graphql_matchers/matchers' require 'rspec/graphql_matchers/types_helper' ================================================ FILE: rspec-graphql_matchers.gemspec ================================================ # frozen_string_literal: true lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'rspec/graphql_matchers/version' Gem::Specification.new do |spec| spec.name = 'rspec-graphql_matchers' spec.version = Rspec::GraphqlMatchers::VERSION spec.authors = ['Samuel Brandão'] spec.email = ['gb.samuel@gmail.com'] spec.summary = 'Collection of rspec matchers to test your graphQL api schema.' spec.homepage = 'https://github.com/khamusa/rspec-graphql_matchers' spec.license = 'MIT' spec.files = `git ls-files -z` .split("\x0") .reject { |f| f.match(%r{^(test|spec|features)/}) } spec.bindir = 'exe' spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] spec.add_dependency 'graphql', '~> 2.0' spec.add_dependency 'rspec', '~> 3.0' spec.add_development_dependency 'bundler', '~> 2.0' # CodeClimate does not yet support SimpleCov 0.18 spec.add_development_dependency 'simplecov', '~>0.17.0' spec.add_development_dependency 'pry', '~> 0' spec.add_development_dependency 'rubocop', '0.71' end ================================================ FILE: spec/rspec/accept_argument_matcher_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' module RSpec module GraphqlMatchers describe 'expect(a_type).to accept_argument(field_name)' do subject(:a_type) do Class.new(GraphQL::Schema::InputObject) do graphql_name 'TestObject' argument :id, GraphQL::Types::String, required: false argument :other, GraphQL::Types::ID, required: true argument :is_test, GraphQL::Types::Boolean, required: false argument :not_camelized, GraphQL::Types::Boolean, required: false, camelize: false end end it { is_expected.to accept_argument(:id) } it 'passes when the type defines the field' do expect(a_type).to accept_argument(:id) end it 'passes with the underscored argument name' do expect(a_type).to accept_argument(:is_test) end it 'passes with the camel cased argument name' do expect(a_type).to accept_argument(:isTest) end it 'matches a non camelized argument with the underscored argument name' do expect(a_type).to accept_argument(:not_camelized) end it 'fails when the type does not define the expected field' do expect { expect(a_type).to accept_argument(:ids) } .to fail_with( 'expected TestObject to accept argument `ids` but no argument was found ' \ 'with that name' ) end it 'fails with a failure message when the type does not define the field' do expect { expect(a_type).to accept_argument(:ids) } .to fail_with( 'expected TestObject to accept argument `ids` but no argument ' \ 'was found with that name' ) end it 'provides a description' do matcher = accept_argument(:id) matcher.matches?(a_type) expect(matcher.description).to eq('accept argument `id`') end describe '.of_type(a_type)' do it 'passes when the type defines the field with correct type as ' \ 'strings' do expect(a_type).to accept_argument(:id).of_type('String') expect(a_type).to accept_argument('other').of_type('ID!') expect(a_type).to accept_argument('other' => 'ID!') end it 'passes when the type defines the field with correct type as ' \ 'graphql objects' do expect(a_type).to accept_argument(:id).of_type(GraphQL::Types::String) expect(a_type).to accept_argument('other').of_type('ID!') end it 'fails when the type defines a field of the wrong type' do expect { expect(a_type).to accept_argument(:id).of_type('String!') } .to fail_with( 'expected TestObject to accept argument `id` ' \ 'of type `String!`, but it was `String`' ) expect { expect(a_type).to accept_argument('other').of_type(GraphQL::Types::Int.to_non_null_type) } .to fail_with( 'expected TestObject to accept argument `other` ' \ 'of type `Int!`, but it was `ID!`' ) expect { expect(a_type).to accept_argument('other' => GraphQL::Types::Int.to_non_null_type) } .to fail_with( 'expected TestObject to accept argument `other` ' \ 'of type `Int!`, but it was `ID!`' ) end context 'when an invalid type is passed' do let(:a_type) { {} } it 'fails with a Runtime error' do expect { expect(a_type).to accept_argument(:id) } .to raise_error( RuntimeError, 'Invalid object {} provided to accept_argument ' \ 'matcher. It does not seem to be a valid GraphQL object type.' ) end end end end end end ================================================ FILE: spec/rspec/accept_arguments_matcher_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' require 'graphql' describe 'expect(a_field).to accept_arguments(arg_name: arg_type, ...)' do subject(:field) do Class.new(GraphQL::Schema::InputObject) do graphql_name 'SomeInputObject' argument :id, GraphQL::Types::ID, required: true argument :name, GraphQL::Types::String, required: true argument :age, GraphQL::Types::Int, required: false argument :is_test, GraphQL::Types::Boolean, required: false argument :not_camelized, GraphQL::Types::Boolean, required: false, camelize: false end end describe '#matches?' do context 'when expecting a single argument with type' do let(:expected_args) { { id: GraphQL::Types::ID.to_non_null_type } } context 'when the field accepts the expected argument name and type' do it { is_expected.to accept_arguments(expected_args) } end context 'the field accepts an argument with the same name but different type' do let(:expected_args) { { id: GraphQL::Types::ID } } it { is_expected.not_to accept_arguments(expected_args) } end context 'the field does not accept the expected args' do let(:expected_args) { { idz: GraphQL::Types::ID.to_non_null_type } } it { is_expected.not_to accept_arguments(expected_args) } end context 'when the expected argument is camelcase' do let(:expected_args) { { isTest: GraphQL::Types::Boolean } } it { is_expected.to accept_arguments(expected_args) } end context 'when the expected argument is underscored' do let(:expected_args) { { is_test: GraphQL::Types::Boolean } } it { is_expected.to accept_arguments(expected_args) } context 'when the actual argument is not camelized' do let(:expected_args) { { not_camelized: GraphQL::Types::Boolean } } it { is_expected.to accept_arguments(expected_args) } end end end context 'when expecting multiple arguments with type' do context 'when the field accepts only one argument with correct name and type' do let(:expected_args) do { id: GraphQL::Types::ID.to_non_null_type, age: GraphQL::Types::Int.to_list_type, name: GraphQL::Types::String } end it { is_expected.not_to accept_arguments(expected_args) } end context 'when the field accepts all but one of the argument expected args' do let(:expected_args) do { id: GraphQL::Types::ID.to_non_null_type, age: GraphQL::Types::Int, name: GraphQL::Types::Float.to_non_null_type } end it { is_expected.not_to accept_arguments(expected_args) } end context 'when the field accepts all arguments with correct type' do let(:expected_args) do { id: GraphQL::Types::ID.to_non_null_type, age: GraphQL::Types::Int, name: GraphQL::Types::String.to_non_null_type } end it { is_expected.to accept_arguments(expected_args) } end end end describe '#description' do let(:matcher) { accept_arguments(expected_args) } subject(:description) do matcher.matches?(field) matcher.description end context 'with a single expected argument with types specified' do let(:expected_args) { { ability: 'Float' } } it 'returns a description with the argument name and type' do expect(description) .to eq('accept arguments ability(Float)') end end context 'with multiple expected arguments with types specified' do let(:expected_args) do { ability: GraphQL::Types::Int, id: GraphQL::Types::Int, some: GraphQL::Types::Boolean } end it 'describes the arguments the field should accept and their types' do expect(description) .to eq('accept arguments ability(Int), id(Int), some(Boolean)') end end end describe '#failure_message' do let(:matcher) { accept_arguments(expected_args) } let(:expected_args) { { will: 'NotMatch' } } subject(:failure_message) do matcher.matches?(field) matcher.failure_message end it 'informs the expected and actual types' do expect(failure_message).to end_with( 'to accept arguments will(NotMatch)' ) end it 'describes the field through #name' do expect(failure_message) .to start_with("expected field 'SomeInputObject' to") end end end ================================================ FILE: spec/rspec/be_of_type_matcher_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' describe 'expect(a_field).to be_of_type(graphql_type)' do scalar_types = { "Boolean" => GraphQL::Types::Boolean, "Int" => GraphQL::Types::Int, "Float" => GraphQL::Types::Float, "String" => GraphQL::Types::String, "ID" => GraphQL::Types::ID } non_nullable_scalar_types = scalar_types.each_with_object({}) do |(string_name, type), result| result["#{string_name}!"] = type.to_non_null_type end all_types = scalar_types.merge(non_nullable_scalar_types) all_types.each do |(graphql_name, scalar_type)| context "when the field has type #{graphql_name}" do subject(:field) { double('GrahQL Field', type: field_type) } let(:field_type) { scalar_type } it "matches a graphQL type object representing #{graphql_name}" do expect(field).to be_of_type(scalar_type) end it "matches the string '#{graphql_name}'" do expect(field).to be_of_type(graphql_name) end it "does not match the string '#{graphql_name.downcase}'" do expect(field).not_to be_of_type(graphql_name.downcase) end scalar_types.each do |(another_graphql_name, another_scalar)| next if another_scalar == scalar_type context "when matching against the type #{another_scalar}" do let(:matcher) { be_of_type(expected_type) } let(:expected_type) { another_scalar } it 'does not match' do expect(matcher.matches?(field)).to be_falsy end describe 'the failure messages' do subject(:failure_message) { matcher.failure_message } before { matcher.matches?(field) } it 'informs the expected and actual types' do expect(failure_message).to end_with( "to be of type '#{another_graphql_name}', but it was '#{graphql_name}'" ) end context 'the field does not respond to #name' do it 'describes the field through #inspect' do expect(failure_message).to start_with( "expected field '#{field.inspect}' to be of type" ) end end context 'the field responds to #name' do before { allow(field).to receive(:name).and_return('AField') } it 'describes the field through #name' do expect(failure_message) .to start_with("expected field 'AField' to be of type") end end end end end end end describe '#description' do let(:matcher) { be_of_type(String) } it %q(is "be of type 'String'") do matcher.matches?(double(type: 'NotMeaningful')) expect(matcher.description).to eq("be of type 'String'") end end end ================================================ FILE: spec/rspec/graphql_matchers_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' describe Rspec::GraphqlMatchers do it 'has a version number' do expect(Rspec::GraphqlMatchers::VERSION).not_to be nil end end ================================================ FILE: spec/rspec/have_a_field_matcher_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' module RSpec module GraphqlMatchers describe 'expect(a_type).to have_a_field(field_name)' do shared_examples 'have a field' do it { is_expected.to have_a_field(:id) } it 'passes when the type defines the field' do expect(a_type).to have_a_field(:id) end it 'passes with the underscored field name' do expect(a_type).to have_a_field(:is_test) end it 'passes with the camel cased field name' do expect(a_type).to have_a_field(:isTest) end it 'matches a non camelized field with the underscored field name' do expect(a_type).to have_a_field(:not_camelized) end it 'fails when the type does not define the expected field' do expect { expect(a_type).to have_a_field(:ids) } .to fail_with( 'expected TestObject to define field `ids` but no field was found ' \ 'with that name' ) end it 'fails with a failure message when the type does not define the field' do expect { expect(a_type).to have_a_field(:ids) } .to fail_with( 'expected TestObject to define field `ids` but no field ' \ 'was found with that name' ) end it 'provides a description' do matcher = have_a_field(:id) matcher.matches?(a_type) expect(matcher.description).to eq('define field `id`') end describe '.that_returns(a_type)' do it 'passes when the type defines the field with correct type as ' \ 'strings' do expect(a_type).to have_a_field(:id).that_returns('ID!') expect(a_type).to have_a_field('other').that_returns('String') expect(a_type).to have_a_field(:is_test).of_type('Boolean') expect(a_type).to have_a_field(:isTest).of_type('Boolean') end it 'passes when the type defines the field with correct type as ' \ 'graphql objects' do expect(a_type).to have_a_field(:id).that_returns('ID!') expect(a_type).to have_a_field('other').of_type(GraphQL::Types::String) expect(a_type).to have_a_field(:is_test).of_type(GraphQL::Types::Boolean) expect(a_type).to have_a_field(:isTest).of_type(GraphQL::Types::Boolean) end it 'fails when the type defines a field of the wrong type' do expect { expect(a_type).to have_a_field(:id).returning('ID') } .to fail_with( 'expected TestObject to define field `id` ' \ 'of type `ID`, but it was `ID!`' ) expect { expect(a_type).to have_a_field('other') .returning(GraphQL::Types::Int.to_non_null_type) } .to fail_with( 'expected TestObject to define field `other` ' \ 'of type `Int!`, but it was `String`' ) end context 'when an invalid type is passed' do let(:a_type) { {} } it 'fails with a Runtime error' do expect { expect(a_type).to have_a_field(:id) } .to raise_error( RuntimeError, 'Invalid object {} provided to have_a_field ' \ 'matcher. It does not seem to be a valid GraphQL object type.' ) end end end describe '.with_hash_key(hash_key)' do it { is_expected.to have_a_field(:other).with_hash_key(:other_on_hash) } it { is_expected.to have_a_field(:other).with_hash_key('other_on_hash') } it 'fails when the hash_key is incorrect' do expect do expect(a_type).to have_a_field(:other).with_hash_key(:whatever) end.to fail_with( 'expected TestObject to define field `other` ' \ 'with hash key `whatever`, but it was `other_on_hash`' ) end end describe '.with_deprecation_reason' do context 'when the field has a deprecation reason' do let(:deprecated_field) { :deprecated_field } context 'with an expected deprecation reason' do it 'passes when the deprecation reasons match' do expect(a_type).to have_a_field(deprecated_field) .with_deprecation_reason('deprecated') end it 'fails when the deprecation reasons do not match' do expect do expect(a_type).to have_a_field(deprecated_field) .with_deprecation_reason('different deprecation reason') end.to fail_with( 'expected TestObject to define field `deprecated_field` with ' \ 'deprecation reason `different deprecation reason`, ' \ 'but it was `deprecated`' ) end end context 'without an expected deprecation reason' do it 'passes' do expect(a_type).to have_a_field(deprecated_field).with_deprecation_reason end end end context 'when the field does not have a deprecation reason' do let(:non_deprecated_field) { :id } it 'fails without an expected deprecation reason' do expect do expect(a_type).to have_a_field(non_deprecated_field) .with_deprecation_reason end.to fail_with( 'expected TestObject to define field `id` with a deprecation reason, ' \ 'but it was not deprecated' ) end it 'fails with an expected deprecation reason' do expect do expect(a_type).to have_a_field(non_deprecated_field) .with_deprecation_reason('deprecated') end.to fail_with( 'expected TestObject to define field `id` with deprecation ' \ 'reason `deprecated`, but it was not deprecated' ) end end end end subject(:a_type) do Class.new(GraphQL::Schema::Object) do graphql_name 'TestObject' field :id, GraphQL::Types::ID, null: false field :other, GraphQL::Types::String, hash_key: :other_on_hash, null: true field :is_test, GraphQL::Types::Boolean, null: true field :not_camelized, GraphQL::Types::String, null: false, camelize: false field :deprecated_field, GraphQL::Types::String, null: true, deprecation_reason: 'deprecated' end end include_examples 'have a field' context 'with fields defined by implementing an interface' do subject(:a_type) do actual_interface = Module.new do include GraphQL::Schema::Interface graphql_name 'ActualInterface' field :other, GraphQL::Types::String, hash_key: :other_on_hash, null: true field :is_test, GraphQL::Types::Boolean, null: true field :not_camelized, GraphQL::Types::String, null: false, camelize: false field :deprecated_field, GraphQL::Types::String, null: true, deprecation_reason: 'deprecated' end Class.new(GraphQL::Schema::Object) do graphql_name 'TestObject' implements actual_interface implements GraphQL::Types::Relay::Node end end include_examples 'have a field' end end end end ================================================ FILE: spec/rspec/have_a_return_field_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' module RSpec module GraphqlMatchers describe 'expect(a_type).to have_a_return_field(field_name)' \ '.that_returns(a_type)' do subject(:a_type) do Class.new(GraphQL::Schema::RelayClassicMutation) do graphql_name 'TestObject' field :id, GraphQL::Types::String field :other, GraphQL::Types::ID, null: false end end it { is_expected.to have_a_return_field(:id) } it 'passes when the type defines the field' do expect(a_type).to have_a_return_field(:id) end it 'fails when the type does not define the expected field' do expect(a_type).not_to have_a_return_field(:ids) end it 'fails with a failure message when the type does not define the field' do expect { expect(a_type).to have_a_return_field(:ids) } .to fail_with( "expected #{a_type.graphql_name} to define field `ids` but no field was " \ 'found with that name' ) end it 'provides a description' do matcher = have_a_return_field(:id) matcher.matches?(a_type) expect(matcher.description).to eq('define field `id`') end it 'passes when the type defines the field with correct type as strings' do expect(a_type).to have_a_return_field(:id).that_returns('String') expect(a_type).to have_a_return_field('other').that_returns('ID!') end it 'passes when the type defines the field with correct type as graphql ' \ 'objects' do expect(a_type).to have_a_return_field(:id).that_returns(GraphQL::Types::String) expect(a_type).to have_a_return_field('other').that_returns(GraphQL::Types::ID.to_non_null_type) end it 'fails when the type defines a field of the wrong type' do expect { expect(a_type).to have_a_return_field(:id).returning('String!') } .to fail_with( "expected #{a_type.graphql_name} to define field `id` of type `String!`, " \ 'but it was `String`' ) expect do expect(a_type).to have_a_return_field('other').returning(GraphQL::Types::Int.to_non_null_type) end.to fail_with( "expected #{a_type.graphql_name} to define field `other` of type `Int!`, " \ 'but it was `ID!`' ) end context 'when an invalid type is passed' do let(:a_type) { double(to_s: 'InvalidObject') } it 'fails with a Runtime error' do expect { expect(a_type).to have_a_return_field(:id) } .to raise_error( RuntimeError, 'Invalid object InvalidObject provided to have_a_field ' \ 'matcher. It does not seem to be a valid GraphQL object type.' ) end end end end end ================================================ FILE: spec/rspec/have_an_input_field_matcher_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' module RSpec module GraphqlMatchers describe 'expect(a_type).to have_an_input_field(field_name)' \ '.that_returns(a_type)' do subject(:a_type) do Class.new(GraphQL::Schema::RelayClassicMutation) do graphql_name 'TestObject' argument :id, GraphQL::Types::String, required: false argument :other, GraphQL::Types::ID, required: true end end it { is_expected.to have_an_input_field(:id) } it 'passes when the type defines the field' do expect(a_type).to have_an_input_field(:id) end it 'fails when the type does not define the expected field' do expect(a_type).not_to have_an_input_field(:ids) end it 'fails with a failure message when the type does not define the field' do expect { expect(a_type).to have_an_input_field(:ids) } .to fail_with( "expected #{a_type.graphql_name} to define field `ids` but no field was " \ 'found with that name' ) end it 'provides a description' do matcher = have_an_input_field(:id) matcher.matches?(a_type) expect(matcher.description).to eq('define field `id`') end it 'passes when the type defines the field with correct type as strings' do expect(a_type).to have_an_input_field(:id).that_returns('String') expect(a_type).to have_an_input_field('other').that_returns('ID!') end it 'passes when the type defines the field with correct type as graphql ' \ 'objects' do expect(a_type).to have_an_input_field(:id).that_returns(GraphQL::Types::String) expect(a_type).to have_an_input_field('other').that_returns(GraphQL::Types::ID.to_non_null_type) end it 'fails when the type defines a field of the wrong type' do expect { expect(a_type).to have_an_input_field(:id).returning('String!') } .to fail_with( "expected #{a_type.graphql_name} to define field `id` of type `String!`, " \ 'but it was `String`' ) expect do expect(a_type).to have_an_input_field('other').returning(GraphQL::Types::Int.to_non_null_type) end.to fail_with( "expected #{a_type.graphql_name} to define field `other` of type `Int!`, " \ 'but it was `ID!`' ) end context 'when an invalid type is passed' do let(:a_type) { double(to_s: 'InvalidObject') } it 'fails with a Runtime error' do expect { expect(a_type).to have_an_input_field(:id) } .to raise_error( RuntimeError, 'Invalid object InvalidObject provided to have_an_input_field ' \ 'matcher. It does not seem to be a valid GraphQL object type.' ) end end end end end ================================================ FILE: spec/rspec/implement_matcher_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' module RSpec module GraphqlMatchers describe 'expect(a_type).to implement(interface_names)' do AnInterface = Module.new do include GraphQL::Schema::Interface graphql_name 'AnInterface' end AnotherInterface = Module.new do include GraphQL::Schema::Interface graphql_name 'AnotherInterface' end AThirdInterface = Module.new do include GraphQL::Schema::Interface graphql_name 'AClassBasedApiInterface' end subject(:a_type) do Class.new(GraphQL::Schema::Object) do graphql_name 'TestObject' implements GraphQL::Types::Relay::Node implements AnInterface implements AThirdInterface end end it { is_expected.to implement('Node') } it { is_expected.to implement('AnInterface') } it { is_expected.to implement('AClassBasedApiInterface') } it { is_expected.to implement('Node', 'AnInterface', 'AClassBasedApiInterface') } it { is_expected.to implement(['Node']) } it { is_expected.to implement(['AnInterface']) } it { is_expected.to implement(%w[Node AnInterface]) } it { is_expected.to implement(GraphQL::Types::Relay::Node, AnInterface) } it do is_expected.to implement([GraphQL::Types::Relay::Node, AnInterface]) end it { is_expected.not_to implement('AnotherInterface') } it { is_expected.not_to implement(['AnotherInterface']) } it { is_expected.not_to implement(AnotherInterface) } it { is_expected.not_to implement([AnotherInterface]) } it 'fails with a message when the type does include the interfaces' do expect { expect(a_type).to implement('AnotherInterface') } .to fail_with( "expected interfaces: AnotherInterface\n" \ 'actual interfaces: Node, AnInterface, AClassBasedApiInterface' ) end it 'provides a description' do matcher = implement('Node, AnInterface') matcher.matches?(a_type) expect(matcher.description).to eq('implement Node, AnInterface') end context 'when an invalid type is passed' do let(:a_type) { double(to_s: 'InvalidObject') } it 'fails with a Runtime error' do expect { expect(a_type).to have_a_field(:id) } .to raise_error( RuntimeError, 'Invalid object InvalidObject provided to have_a_field matcher. ' \ 'It does not seem to be a valid GraphQL object type.' ) end end end end end ================================================ FILE: spec/rspec/readme_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' describe 'The readme Examples' do ruby_code_regex = /```ruby(.*?)```/m readme_content = File.read( File.expand_path( '../../README.md', __dir__ ) ) # rubocop:disable Security/Eval readme_content.scan(ruby_code_regex) do |ruby_code| eval(ruby_code[0]) end # rubocop:enable Security/Eval end ================================================ FILE: spec/spec_helper.rb ================================================ # frozen_string_literal: true require 'pry' require 'simplecov' SimpleCov.start $LOAD_PATH.unshift File.expand_path('../lib', __dir__) require 'rspec/graphql_matchers' # rubocop:disable Style/MixinUsage include RSpec::GraphqlMatchers::TypesHelper # rubocop:enable Style/MixinUsage module RSpec module Matchers def fail_with(message) raise_error(RSpec::Expectations::ExpectationNotMetError, message) end end end