├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yml │ ├── dynamic-readme.yml │ ├── dynamic-security.yml │ └── rubocop.yml ├── .gitignore ├── .python-version ├── .rubocop.yml ├── .ruby-version ├── .tool-versions ├── .yardopts ├── Appraisals ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── MAINTAINING.md ├── README.md ├── REPRODUCTION_SCRIPT.rb ├── Rakefile ├── SECURITY.md ├── bin └── setup ├── custom_plan.rb ├── doc_config ├── gh-pages │ └── index.html.erb └── yard │ ├── setup.rb │ └── templates │ └── default │ ├── fulldoc │ └── html │ │ ├── css │ │ ├── bootstrap.css │ │ ├── full_list.css │ │ ├── global.css │ │ ├── solarized.css │ │ └── style.css │ │ ├── full_list.erb │ │ ├── full_list_class.erb │ │ ├── full_list_method.erb │ │ ├── js │ │ ├── app.js │ │ ├── full_list.js │ │ ├── jquery.stickyheaders.js │ │ └── underscore.min.js │ │ └── setup.rb │ ├── layout │ └── html │ │ ├── breadcrumb.erb │ │ ├── fonts.erb │ │ ├── footer.erb │ │ ├── layout.erb │ │ ├── search.erb │ │ └── setup.rb │ ├── method_details │ └── html │ │ └── source.erb │ └── module │ └── html │ └── box_info.erb ├── docs └── errors │ └── NonCaseSwappableValueError.md ├── gemfiles ├── rails_6_1.gemfile ├── rails_6_1.gemfile.lock ├── rails_7_0.gemfile ├── rails_7_0.gemfile.lock ├── rails_7_1.gemfile ├── rails_7_1.gemfile.lock ├── rails_7_2.gemfile └── rails_7_2.gemfile.lock ├── lib ├── shoulda-matchers.rb └── shoulda │ ├── matchers.rb │ └── matchers │ ├── action_controller.rb │ ├── action_controller │ ├── callback_matcher.rb │ ├── filter_param_matcher.rb │ ├── flash_store.rb │ ├── permit_matcher.rb │ ├── redirect_to_matcher.rb │ ├── render_template_matcher.rb │ ├── render_with_layout_matcher.rb │ ├── rescue_from_matcher.rb │ ├── respond_with_matcher.rb │ ├── route_matcher.rb │ ├── route_params.rb │ ├── session_store.rb │ ├── set_flash_matcher.rb │ ├── set_session_matcher.rb │ └── set_session_or_flash_matcher.rb │ ├── active_model.rb │ ├── active_model │ ├── allow_value_matcher.rb │ ├── allow_value_matcher │ │ ├── attribute_changed_value_error.rb │ │ ├── attribute_does_not_exist_error.rb │ │ ├── attribute_setter.rb │ │ ├── attribute_setter_and_validator.rb │ │ ├── attribute_setters.rb │ │ ├── attribute_setters_and_validators.rb │ │ ├── successful_check.rb │ │ └── successful_setting.rb │ ├── comparison_matcher.rb │ ├── disallow_value_matcher.rb │ ├── errors.rb │ ├── have_secure_password_matcher.rb │ ├── helpers.rb │ ├── numericality_matchers.rb │ ├── numericality_matchers │ │ ├── even_number_matcher.rb │ │ ├── numeric_type_matcher.rb │ │ ├── odd_number_matcher.rb │ │ ├── only_integer_matcher.rb │ │ ├── range_matcher.rb │ │ └── submatchers.rb │ ├── qualifiers.rb │ ├── qualifiers │ │ ├── allow_blank.rb │ │ ├── allow_nil.rb │ │ ├── ignore_interference_by_writer.rb │ │ └── ignoring_interference_by_writer.rb │ ├── validate_absence_of_matcher.rb │ ├── validate_acceptance_of_matcher.rb │ ├── validate_comparison_of_matcher.rb │ ├── validate_confirmation_of_matcher.rb │ ├── validate_exclusion_of_matcher.rb │ ├── validate_inclusion_of_matcher.rb │ ├── validate_length_of_matcher.rb │ ├── validate_numericality_of_matcher.rb │ ├── validate_presence_of_matcher.rb │ ├── validation_matcher.rb │ ├── validation_matcher │ │ └── build_description.rb │ ├── validation_message_finder.rb │ └── validator.rb │ ├── active_record.rb │ ├── active_record │ ├── accept_nested_attributes_for_matcher.rb │ ├── association_matcher.rb │ ├── association_matchers.rb │ ├── association_matchers │ │ ├── counter_cache_matcher.rb │ │ ├── dependent_matcher.rb │ │ ├── inverse_of_matcher.rb │ │ ├── join_table_matcher.rb │ │ ├── model_reflection.rb │ │ ├── model_reflector.rb │ │ ├── option_verifier.rb │ │ ├── optional_matcher.rb │ │ ├── order_matcher.rb │ │ ├── required_matcher.rb │ │ ├── source_matcher.rb │ │ └── through_matcher.rb │ ├── define_enum_for_matcher.rb │ ├── encrypt_matcher.rb │ ├── have_attached_matcher.rb │ ├── have_db_column_matcher.rb │ ├── have_db_index_matcher.rb │ ├── have_implicit_order_column.rb │ ├── have_readonly_attribute_matcher.rb │ ├── have_rich_text_matcher.rb │ ├── have_secure_token_matcher.rb │ ├── normalize_matcher.rb │ ├── serialize_matcher.rb │ ├── uniqueness.rb │ ├── uniqueness │ │ ├── model.rb │ │ ├── namespace.rb │ │ ├── test_model_creator.rb │ │ └── test_models.rb │ └── validate_uniqueness_of_matcher.rb │ ├── configuration.rb │ ├── doublespeak.rb │ ├── doublespeak │ ├── double.rb │ ├── double_collection.rb │ ├── double_implementation_registry.rb │ ├── method_call.rb │ ├── object_double.rb │ ├── proxy_implementation.rb │ ├── stub_implementation.rb │ └── world.rb │ ├── error.rb │ ├── independent.rb │ ├── independent │ ├── delegate_method_matcher.rb │ └── delegate_method_matcher │ │ └── target_not_defined_error.rb │ ├── integrations.rb │ ├── integrations │ ├── configuration.rb │ ├── configuration_error.rb │ ├── inclusion.rb │ ├── libraries.rb │ ├── libraries │ │ ├── action_controller.rb │ │ ├── active_model.rb │ │ ├── active_record.rb │ │ ├── missing_library.rb │ │ ├── rails.rb │ │ └── routing.rb │ ├── rails.rb │ ├── registry.rb │ ├── test_frameworks.rb │ └── test_frameworks │ │ ├── active_support_test_case.rb │ │ ├── minitest_4.rb │ │ ├── minitest_5.rb │ │ ├── missing_test_framework.rb │ │ ├── rspec.rb │ │ └── test_unit.rb │ ├── matcher_context.rb │ ├── rails_shim.rb │ ├── routing.rb │ ├── util.rb │ ├── util │ └── word_wrap.rb │ ├── version.rb │ └── warn.rb ├── script ├── install_gems_in_all_appraisals ├── run_all_tests ├── supported_ruby_versions ├── update_gem_in_all_appraisals └── update_gems_in_all_appraisals ├── shoulda-matchers.gemspec ├── spec ├── acceptance │ ├── active_model_integration_spec.rb │ ├── active_record_integration_spec.rb │ ├── independent_matchers_spec.rb │ ├── multiple_libraries_integration_spec.rb │ └── rails_integration_spec.rb ├── acceptance_spec_helper.rb ├── doublespeak_spec_helper.rb ├── report_warnings.rb ├── spec_helper.rb ├── support │ ├── acceptance │ │ ├── adds_shoulda_matchers_to_project.rb │ │ ├── helpers.rb │ │ ├── helpers │ │ │ ├── active_model_helpers.rb │ │ │ ├── active_record_helpers.rb │ │ │ ├── array_helpers.rb │ │ │ ├── base_helpers.rb │ │ │ ├── command_helpers.rb │ │ │ ├── file_helpers.rb │ │ │ ├── gem_helpers.rb │ │ │ ├── minitest_helpers.rb │ │ │ ├── n_unit_helpers.rb │ │ │ ├── pluralization_helpers.rb │ │ │ ├── rails_migration_helpers.rb │ │ │ ├── rails_version_helpers.rb │ │ │ ├── rspec_helpers.rb │ │ │ ├── ruby_version_helpers.rb │ │ │ └── step_helpers.rb │ │ └── matchers │ │ │ ├── have_output.rb │ │ │ ├── indicate_number_of_tests_was_run_matcher.rb │ │ │ └── indicate_that_tests_were_run_matcher.rb │ ├── tests │ │ ├── bundle.rb │ │ ├── command_runner.rb │ │ ├── current_bundle.rb │ │ ├── database.rb │ │ ├── database_adapters │ │ │ ├── config │ │ │ │ ├── postgresql.yml │ │ │ │ └── sqlite3.yml │ │ │ ├── postgresql.rb │ │ │ └── sqlite3.rb │ │ ├── database_configuration.rb │ │ ├── database_configuration_registry.rb │ │ ├── filesystem.rb │ │ └── version.rb │ └── unit │ │ ├── active_record │ │ └── create_table.rb │ │ ├── attribute.rb │ │ ├── capture.rb │ │ ├── change_value.rb │ │ ├── configuration.rb │ │ ├── create_model_arguments │ │ ├── basic.rb │ │ ├── has_many.rb │ │ └── uniqueness_matcher.rb │ │ ├── helpers │ │ ├── active_model_helpers.rb │ │ ├── active_model_versions.rb │ │ ├── active_record_versions.rb │ │ ├── active_resource_builder.rb │ │ ├── allow_value_matcher_helpers.rb │ │ ├── application_configuration_helpers.rb │ │ ├── class_builder.rb │ │ ├── column_type_helpers.rb │ │ ├── confirmation_matcher_helpers.rb │ │ ├── controller_builder.rb │ │ ├── database_helpers.rb │ │ ├── i18n_faker.rb │ │ ├── mailer_builder.rb │ │ ├── message_helpers.rb │ │ ├── model_builder.rb │ │ ├── rails_versions.rb │ │ └── validation_matcher_scenario_helpers.rb │ │ ├── i18n.rb │ │ ├── load_environment.rb │ │ ├── matchers │ │ ├── deprecate.rb │ │ ├── fail_with_message_including_matcher.rb │ │ ├── fail_with_message_matcher.rb │ │ ├── match_against.rb │ │ └── print_warning_including.rb │ │ ├── model_creation_strategies │ │ ├── active_model.rb │ │ └── active_record.rb │ │ ├── model_creators.rb │ │ ├── model_creators │ │ ├── active_model.rb │ │ ├── active_record.rb │ │ ├── active_record │ │ │ ├── has_and_belongs_to_many.rb │ │ │ ├── has_many.rb │ │ │ └── uniqueness_matcher.rb │ │ └── basic.rb │ │ ├── rails_application.rb │ │ ├── record_builder_with_i18n_validation_message.rb │ │ ├── record_validating_confirmation_builder.rb │ │ ├── record_with_different_error_attribute_builder.rb │ │ ├── record_with_unrelated_error_builder.rb │ │ ├── shared_examples │ │ ├── ignoring_interference_by_writer.rb │ │ ├── numerical_submatcher.rb │ │ └── set_session_or_flash.rb │ │ └── validation_matcher_scenario.rb ├── unit │ └── shoulda │ │ └── matchers │ │ ├── action_controller │ │ ├── callback_matcher_spec.rb │ │ ├── filter_param_matcher_spec.rb │ │ ├── permit_matcher_spec.rb │ │ ├── redirect_to_matcher_spec.rb │ │ ├── render_template_matcher_spec.rb │ │ ├── render_with_layout_matcher_spec.rb │ │ ├── rescue_from_matcher_spec.rb │ │ ├── respond_with_matcher_spec.rb │ │ ├── route_matcher_spec.rb │ │ ├── route_params_spec.rb │ │ ├── set_flash_matcher_spec.rb │ │ ├── set_session_matcher_spec.rb │ │ └── set_session_or_flash_matcher_spec.rb │ │ ├── active_model │ │ ├── allow_value_matcher_spec.rb │ │ ├── disallow_value_matcher_spec.rb │ │ ├── have_secure_password_matcher_spec.rb │ │ ├── helpers_spec.rb │ │ ├── validate_absence_of_matcher_spec.rb │ │ ├── validate_acceptance_of_matcher_spec.rb │ │ ├── validate_comparison_of_matcher_spec.rb │ │ ├── validate_confirmation_of_matcher_spec.rb │ │ ├── validate_exclusion_of_matcher_spec.rb │ │ ├── validate_inclusion_of_matcher_spec.rb │ │ ├── validate_length_of_matcher_spec.rb │ │ ├── validate_numericality_of_matcher_spec.rb │ │ └── validate_presence_of_matcher_spec.rb │ │ ├── active_record │ │ ├── accept_nested_attributes_for_matcher_spec.rb │ │ ├── association_matcher_spec.rb │ │ ├── association_matchers │ │ │ └── model_reflection_spec.rb │ │ ├── define_enum_for_matcher_spec.rb │ │ ├── encrypt_matcher_spec.rb │ │ ├── have_attached_matcher_spec.rb │ │ ├── have_db_column_matcher_spec.rb │ │ ├── have_db_index_matcher_spec.rb │ │ ├── have_implicit_order_column_spec.rb │ │ ├── have_readonly_attributes_matcher_spec.rb │ │ ├── have_rich_text_matcher_spec.rb │ │ ├── have_secure_token_matcher_spec.rb │ │ ├── normalize_matcher_spec.rb │ │ ├── serialize_matcher_spec.rb │ │ └── validate_uniqueness_of_matcher_spec.rb │ │ ├── doublespeak │ │ ├── double_collection_spec.rb │ │ ├── double_implementation_registry_spec.rb │ │ ├── double_spec.rb │ │ ├── object_double_spec.rb │ │ ├── proxy_implementation_spec.rb │ │ ├── stub_implementation_spec.rb │ │ └── world_spec.rb │ │ ├── doublespeak_spec.rb │ │ ├── independent │ │ └── delegate_method_matcher_spec.rb │ │ ├── routing │ │ └── route_matcher_spec.rb │ │ └── util │ │ └── word_wrap_spec.rb └── unit_spec_helper.rb ├── tasks └── documentation.rb └── zeus.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | <!-- By contributing to this project, you agree to abide by the thoughtbot Code 11 | of Conduct: https://thoughtbot.com/open-source-code-of-conduct --> 12 | 13 | ### Description 14 | 15 | <!-- A clear and concise description of what the bug is. --> 16 | 17 | ### Reproduction Steps 18 | 19 | <!-- Steps for others to reproduce the bug. Be as specific as possible. A 20 | reproduction script or link to a sample application that demonstrates the 21 | problem are especially helpful. --> 22 | 23 | <!-- You can create a reproduction script by copying this sample reproduction 24 | script and adding whatever code is necessary to get a failing test case: 25 | https://github.com/thoughtbot/shoulda-matchers/blob/main/REPRODUCTION_SCRIPT.rb --> 26 | 27 | ### Expected behavior 28 | 29 | <!-- What you expected to happen. --> 30 | 31 | ### Actual behavior 32 | 33 | <!-- What happened instead. --> 34 | 35 | ### System configuration 36 | **shoulda_matchers version**: 37 | **rails version**: 38 | **ruby version**: 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | <!-- By contributing to this project, you agree to abide by the thoughtbot Code 11 | of Conduct: https://thoughtbot.com/open-source-code-of-conduct --> 12 | 13 | ### Problem this feature will solve 14 | 15 | <!-- A clear and concise description of what the problem is. Ex. When doing 16 | [...] I find it difficult to [...] --> 17 | 18 | ### Desired solution 19 | 20 | <!-- The feature or change that would solve the problem --> 21 | 22 | ## Alternatives considered 23 | 24 | <!-- Any alternative solutions or features you've considered. --> 25 | 26 | ## Additional context 27 | 28 | <!-- Add any other context about this feature request. --> 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | types: 11 | - opened 12 | - synchronize 13 | paths-ignore: 14 | - '**.md' 15 | 16 | jobs: 17 | build: 18 | services: 19 | postgres: 20 | image: postgres 21 | env: 22 | POSTGRES_PASSWORD: postgres 23 | ports: [ '5432:5432' ] 24 | options: --health-cmd pg_isready --health-interval 2s --health-timeout 1s --health-retries 10 25 | runs-on: ubuntu-latest 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | ruby: 30 | - 3.3.0 31 | - 3.2.2 32 | - 3.1.4 33 | - 3.0.6 34 | appraisal: 35 | - rails_7_2 36 | - rails_7_1 37 | - rails_7_0 38 | - rails_6_1 39 | adapter: 40 | - sqlite3 41 | - postgresql 42 | exclude: 43 | - { ruby: 3.3.0, appraisal: rails_6_1 } 44 | - { ruby: 3.3.0, appraisal: rails_7_0 } 45 | - { ruby: 3.2.2, appraisal: rails_6_1 } 46 | - { ruby: 3.0.6, appraisal: rails_7_0 } 47 | - { ruby: 3.0.6, appraisal: rails_7_1 } 48 | - { ruby: 3.0.6, appraisal: rails_7_2 } 49 | env: 50 | DATABASE_ADAPTER: ${{ matrix.adapter }} 51 | BUNDLE_GEMFILE: gemfiles/${{ matrix.appraisal }}.gemfile 52 | steps: 53 | - uses: actions/checkout@v3 54 | - name: Set up Ruby 55 | id: set-up-ruby 56 | uses: ruby/setup-ruby@v1 57 | with: 58 | ruby-version: ${{ matrix.ruby }} 59 | - uses: actions/cache@v3 60 | with: 61 | path: vendor/bundle 62 | key: v1-rubygems-local-${{ runner.os }}-${{ matrix.ruby }}-${{ hashFiles(format('gemfiles/{0}.gemfile.lock', matrix.appraisal)) }} 63 | - name: Install dependencies 64 | run: bundle install --jobs=3 --retry=3 65 | - name: Run Unit Tests 66 | run: RUBYOPT='--enable=frozen-string-literal' bundle exec rake spec:unit --trace 67 | - name: Run Acceptance Tests 68 | run: RUBYOPT='--enable=frozen-string-literal' bundle exec rake spec:acceptance --trace 69 | -------------------------------------------------------------------------------- /.github/workflows/dynamic-readme.yml: -------------------------------------------------------------------------------- 1 | name: update-templates 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | update-templates: 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | pages: write 15 | uses: thoughtbot/templates/.github/workflows/dynamic-readme.yaml@main 16 | secrets: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/dynamic-security.yml: -------------------------------------------------------------------------------- 1 | name: update-security 2 | 3 | on: 4 | push: 5 | paths: 6 | - SECURITY.md 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | update-security: 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | pages: write 17 | uses: thoughtbot/templates/.github/workflows/dynamic-security.yaml@main 18 | secrets: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | name: RuboCop 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout Repository 11 | uses: actions/checkout@v3 12 | 13 | - name: Setup Ruby 14 | uses: ruby/setup-ruby@v1 15 | 16 | - name: Cache gems 17 | uses: actions/cache@v3 18 | with: 19 | path: ../vendor/bundle 20 | key: ${{ runner.os }}-rubocop-${{ hashFiles('**/Gemfile.lock') }} 21 | restore-keys: | 22 | ${{ runner.os }}-rubocop- 23 | 24 | - name: Install gems 25 | run: | 26 | bundle config path ../vendor/bundle 27 | bundle install --jobs 4 --retry 3 28 | 29 | - name: Run RuboCop 30 | run: bundle exec rubocop --parallel 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | .gh-pages 3 | .thoughtbot-gh-pages 4 | .mcmire-gh-pages 5 | .yardoc 6 | coverage 7 | build 8 | doc 9 | pkg 10 | source 11 | spec/examples.txt 12 | tmp 13 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 2.7.15 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.0 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.3.0 2 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --protected 3 | --readme README.md 4 | --markup markdown 5 | --hide-tag return 6 | --hide-tag param 7 | -e ./doc_config/yard/setup.rb 8 | - 9 | CHANGELOG.md 10 | docs/**/*.md 11 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # More details are here: https://help.github.com/articles/about-codeowners/ 5 | 6 | # The '*' pattern is global owners. 7 | 8 | # Order is important. The last matching pattern has the most precedence. 9 | # The folders are ordered as follows: 10 | 11 | # In each subsection folders are ordered first by depth, then alphabetically. 12 | # This should make it easy to add new rules without breaking existing ones. 13 | 14 | # Global rule: 15 | * @matsales28 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'appraisal', '2.5.0' 4 | gem 'bundler', '~> 2.0' 5 | gem 'pry' 6 | gem 'pry-byebug' 7 | gem 'rake', '13.0.1' 8 | gem 'rspec', '~> 3.9' 9 | gem 'rubocop', require: false 10 | gem 'rubocop-packaging', require: false 11 | gem 'rubocop-rails', require: false 12 | gem 'warnings_logger' 13 | gem 'zeus', require: false 14 | 15 | # YARD 16 | gem 'fssm' 17 | gem 'redcarpet' 18 | gem 'rouge' 19 | gem 'yard' 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Tammer Saleh and thoughtbot, inc. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /REPRODUCTION_SCRIPT.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile(true) do 4 | source 'https://rubygems.org' 5 | gem 'shoulda-matchers' 6 | gem 'activerecord' 7 | gem 'sqlite3' 8 | gem 'rspec' 9 | end 10 | 11 | require 'active_record' 12 | require 'shoulda-matchers' 13 | require 'logger' 14 | 15 | ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') 16 | ActiveRecord::Base.logger = Logger.new(STDOUT) 17 | 18 | # TODO: Update the schema to include the specific tables or columns necessary 19 | # to reproduce the bug 20 | ActiveRecord::Schema.define do 21 | create_table :posts, force: true do |t| 22 | t.string :body 23 | end 24 | end 25 | 26 | Shoulda::Matchers.configure do |config| 27 | config.integrate do |with| 28 | with.test_framework :rspec 29 | 30 | with.library :active_record 31 | with.library :active_model 32 | end 33 | end 34 | 35 | RSpec.configure do |config| 36 | config.include Shoulda::Matchers::ActiveRecord 37 | config.include Shoulda::Matchers::ActiveModel 38 | config.include Shoulda::Matchers::ActionController 39 | end 40 | 41 | # TODO: Add any application specific code necessary to reproduce the bug 42 | class Post < ActiveRecord::Base 43 | validates :body, uniqueness: true 44 | end 45 | 46 | # TODO: Write a failing test case to demonstrate what isn't working as 47 | # expected 48 | RSpec.describe Post do 49 | describe 'validations' do 50 | it { is_expected.to validate_uniqueness_of(:body) } 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'bundler/gem_tasks' 3 | require 'rspec/core/rake_task' 4 | require 'appraisal' 5 | require_relative 'tasks/documentation' 6 | require_relative 'spec/support/tests/database' 7 | require_relative 'spec/support/tests/current_bundle' 8 | 9 | RSpec::Core::RakeTask.new('spec:unit') do |t| 10 | t.ruby_opts = '-w -r ./spec/report_warnings' 11 | t.pattern = 'spec/unit/**/*_spec.rb' 12 | t.rspec_opts = '--color --format progress' 13 | t.verbose = false 14 | end 15 | 16 | RSpec::Core::RakeTask.new('spec:acceptance') do |t| 17 | t.ruby_opts = '-w -r ./spec/report_warnings' 18 | t.pattern = 'spec/acceptance/**/*_spec.rb' 19 | t.rspec_opts = '--color --format progress' 20 | t.verbose = false 21 | end 22 | 23 | task :default do 24 | if Tests::CurrentBundle.instance.appraisal_in_use? 25 | sh 'rake spec:unit --trace' 26 | sh 'rake spec:acceptance --trace' 27 | elsif ENV['CI'] 28 | exec 'appraisal install && appraisal rake --trace' 29 | else 30 | appraisal = Tests::CurrentBundle.instance.latest_appraisal 31 | exec "appraisal install && appraisal #{appraisal} rake --trace" 32 | end 33 | end 34 | 35 | namespace :appraisal do 36 | task list: :environment do 37 | appraisals = Tests::CurrentBundle.instance.available_appraisals 38 | puts "Valid appraisals: #{appraisals.join(', ')}" 39 | end 40 | end 41 | 42 | Shoulda::Matchers::DocumentationTasks.create 43 | 44 | task release: 'docs:publish_latest' 45 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | <!-- START /templates/security.md --> 2 | # Security Policy 3 | 4 | ## Supported Versions 5 | 6 | Only the the latest version of this project is supported at a given time. If 7 | you find a security issue with an older version, please try updating to the 8 | latest version first. 9 | 10 | If for some reason you can't update to the latest version, please let us know 11 | your reasons so that we can have a better understanding of your situation. 12 | 13 | ## Reporting a Vulnerability 14 | 15 | For security inquiries or vulnerability reports, visit 16 | <https://thoughtbot.com/security>. 17 | 18 | If you have any suggestions to improve this policy, visit <https://thoughtbot.com/security>. 19 | 20 | <!-- END /templates/security.md --> 21 | -------------------------------------------------------------------------------- /doc_config/gh-pages/index.html.erb: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html> 3 | <head> 4 | <title>shoulda-matchers Documentation - latest</title> 5 | <meta http-equiv="refresh" content="0;URL=<%= locals[:ref] %>"> 6 | </head> 7 | <body> 8 | </body> 9 | </html> 10 | -------------------------------------------------------------------------------- /doc_config/yard/setup.rb: -------------------------------------------------------------------------------- 1 | YARD::Templates::Engine.register_template_path( 2 | "#{File.dirname(__FILE__)}/templates", 3 | ) 4 | 5 | require 'rouge' 6 | 7 | module YARD 8 | module Templates 9 | module Helpers 10 | module HtmlSyntaxHighlightHelper 11 | def html_syntax_highlight_ruby(source) 12 | highlight(:ruby, source) 13 | end 14 | 15 | private 16 | 17 | def highlight(language, source) 18 | lexer = Rouge::Lexers.const_get(language.capitalize) 19 | Rouge::Formatters::HTML.new.format(lexer.new.lex(source)) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/fulldoc/html/css/full_list.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 14px; 3 | padding: 20px; 4 | } 5 | 6 | h1 { 7 | font-size: 1.5em; 8 | } 9 | 10 | .search_info, .toggle { 11 | display: none; 12 | } 13 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/fulldoc/html/css/global.css: -------------------------------------------------------------------------------- 1 | @import "https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,300italic,400,400italic,600,600italic,800|Droid+Sans+Mono"; 2 | 3 | body { 4 | font-size: 16px; 5 | line-height: 1.5; 6 | } 7 | 8 | a, a:hover { 9 | color: #136cc6; 10 | } 11 | 12 | h1, h2, h3, h4, h5, h6, p, pre { 13 | margin-bottom: 1em; 14 | margin-top: 0; 15 | } 16 | 17 | h1, h2, h3, h4, h5, h6, body { 18 | font-family: "Source Sans Pro", sans-serif; 19 | } 20 | 21 | h1, h2, h3, h4, h5, h6 { 22 | font-weight: 800; 23 | } 24 | 25 | pre, tt, code { 26 | background: #FFFBF4; 27 | border-radius: 3px; 28 | border: 1px solid rgba(0,0,0,0.1); 29 | font-family: "Droid Sans Mono", monospace; 30 | font-size: 13px; 31 | } 32 | 33 | pre code { 34 | border: none; 35 | } 36 | 37 | tt, code { 38 | color: black; 39 | padding: 0 4px; 40 | } 41 | 42 | ul, ol { 43 | margin-left: 1em; 44 | padding-left: 1em; 45 | } 46 | 47 | p, blockquote { 48 | margin-bottom: 1.25em; 49 | } 50 | 51 | blockquote { 52 | font-style: italic; 53 | padding-top: 0; 54 | padding-bottom: 0; 55 | padding-left: 1em; 56 | } 57 | 58 | blockquote p { 59 | font-size: inherit; 60 | font-weight: inherit; 61 | line-height: inherit; 62 | } 63 | 64 | /* 65 | ul ul, ol ol, ul ol, ol ul { 66 | margin-bottom: 1.25em; 67 | } 68 | */ 69 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/fulldoc/html/full_list.erb: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 2 | "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 3 | <html> 4 | <head> 5 | <meta http-equiv="Content-Type" content="text/html; charset=<%= charset %>" /> 6 | <% stylesheets_full_list.each do |stylesheet| %> 7 | <link rel="stylesheet" href="<%= url_for(stylesheet) %>" type="text/css" media="screen" charset="utf-8" /> 8 | <% end %> 9 | 10 | <% javascripts_full_list.each do |javascript| %> 11 | <script type="text/javascript" charset="utf-8" src="<%= url_for(javascript) %>"></script> 12 | <% end %> 13 | 14 | <title><%= @list_title %></title> 15 | <base id="base_target" target="_parent" /> 16 | </head> 17 | <body> 18 | <div id="content"> 19 | <h1 id="full_list_header"><%= @list_title %></h1> 20 | 21 | <ul id="full_list" class="<%= @list_class || @list_type %>"> 22 | <%= erb "full_list_#{@list_type}" %> 23 | </ul> 24 | </div> 25 | </body> 26 | </html> 27 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/fulldoc/html/full_list_class.erb: -------------------------------------------------------------------------------- 1 | <%= class_list %> 2 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/fulldoc/html/full_list_method.erb: -------------------------------------------------------------------------------- 1 | <% n = 1 %> 2 | <% @items.each do |item| %> 3 | <li class="r<%= n %> <%= item.has_tag?(:deprecated) ? 'deprecated' : '' %>"> 4 | <%= linkify item, h(item.name(true)) %> 5 | <small>(<%= item.namespace.title %>)</small> 6 | </li> 7 | <% n = n == 2 ? 1 : 2 %> 8 | <% end %> 9 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/fulldoc/html/js/full_list.js: -------------------------------------------------------------------------------- 1 | // Override with nothing 2 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/fulldoc/html/setup.rb: -------------------------------------------------------------------------------- 1 | def stylesheets_full_list 2 | %w(css/solarized.css css/bootstrap.css css/global.css) + super 3 | end 4 | 5 | def javascripts 6 | javascripts = super 7 | javascripts.insert 1, 'js/jquery.stickyheaders.js' 8 | end 9 | 10 | def class_list(root = Registry.root, tree = TreeContext.new) 11 | out = String.new('') 12 | children = run_verifier(root.children) 13 | if root == Registry.root 14 | children += @items.select {|o| o.namespace.is_a?(CodeObjects::Proxy) } 15 | end 16 | children.compact.sort_by(&:path).each do |child| 17 | next unless child.is_a?(CodeObjects::NamespaceObject) 18 | 19 | name = child.namespace.is_a?(CodeObjects::Proxy) ? child.path : child.name 20 | has_children = run_verifier(child.children). 21 | any? {|o| o.is_a?(CodeObjects::NamespaceObject) } 22 | out << "<li id='object_#{child.path}' class='#{tree.classes.join(' ')}'>" 23 | out << "<div class='item'>" 24 | out << "<a class='toggle'></a> " if has_children 25 | out << linkify(child, name) 26 | if child.is_a?(CodeObjects::ClassObject) && child.superclass 27 | out << " < #{child.superclass.name}" 28 | end 29 | out << "<small class='search_info'>" 30 | out << child.namespace.title 31 | out << '</small>' 32 | out << '</div>' 33 | tree.nest do 34 | out << "<ul>#{class_list(child, tree)}</ul>" if has_children 35 | end 36 | out << '</li>' 37 | end 38 | out 39 | end 40 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/layout/html/breadcrumb.erb: -------------------------------------------------------------------------------- 1 | <div id="menu"> 2 | <% unless @file && @file.filename == 'README.md' %> 3 | <span class="title"> 4 | <%= linkify('file:README.md', 'Home') %> 5 | </span> » 6 | <% end %> 7 | <% if @contents || @file %> 8 | <span class="title"><%= @breadcrumb_title.sub(/\AFile: /, "") %></span> 9 | <% elsif object.is_a?(CodeObjects::Base) %> 10 | <%= @breadcrumb.map {|obj| "<span class='title'>" + linkify(obj, obj.name) + "</span>" }.join(" » ") %> 11 | <%= @breadcrumb.size > 0 ? " » " : "" %> 12 | <span class="title"><%= object.root? ? "Top Level Namespace" : object.name(true) %></span> 13 | <% end %> 14 | </div> 15 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/layout/html/fonts.erb: -------------------------------------------------------------------------------- 1 | <!-- Fonts go here --> 2 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/layout/html/footer.erb: -------------------------------------------------------------------------------- 1 | <div id="footer"> 2 | Generated on 3 | <%= Time.now.strftime("%B %-d, %Y") %> 4 | by 5 | <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">YARD</a>. 6 | </div> 7 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/layout/html/layout.erb: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 2 | "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 3 | <html xmlns="https://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> 4 | <head> 5 | <%= erb(:headers) %> 6 | </head> 7 | <body> 8 | <div id="header"> 9 | <div class="header-row"> 10 | <%= erb(:breadcrumb) %> 11 | <%= erb(:search) %> 12 | <div class="clear"></div> 13 | </div> 14 | </div> 15 | 16 | <div id="main"> 17 | <div id="content"><%= yieldall %></div> 18 | 19 | <%= erb(:footer) %> 20 | </div> 21 | 22 | </body> 23 | </html> 24 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/layout/html/search.erb: -------------------------------------------------------------------------------- 1 | <div id="search" class="js-search"> 2 | <ul> 3 | <% menu_lists.each do |field| %> 4 | <li> 5 | <a href="<%= url_for_list(field[:type]) %>"> 6 | <%= field[:search_title] %> 7 | </a> 8 | </li> 9 | <% end %> 10 | </ul> 11 | 12 | <iframe id="search_frame" class="js-search-frame"></iframe> 13 | </div> 14 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/layout/html/setup.rb: -------------------------------------------------------------------------------- 1 | def stylesheets 2 | %w(css/solarized.css css/bootstrap.css css/global.css) + super 3 | end 4 | 5 | def javascripts 6 | javascripts = super 7 | javascripts.insert 1, 'js/jquery.stickyheaders.js', 'js/underscore.min.js' 8 | end 9 | 10 | def diskfile 11 | @file.attributes[:markup] ||= markup_for_file('', @file.filename) 12 | 13 | contents = 14 | if @file.filename == 'README.md' 15 | preprocess_index(@file.contents) 16 | else 17 | @file.contents 18 | end 19 | 20 | data = htmlify(contents, @file.attributes[:markup]) 21 | "<div id='filecontents'>#{data}</div>" 22 | end 23 | 24 | def preprocess_index(contents) 25 | regex = /\[ (\w+) \] \( lib \/ ([^()]+) \.rb (?:\#L\d+)? \)/x 26 | 27 | contents.gsub(regex) do 28 | method_name = $1 29 | file_path = $2 30 | 31 | module_name = file_path.split('/')[0..2]. 32 | map do |value| 33 | value. 34 | split('_'). 35 | map { |word| word[0].upcase + word[1..] }. 36 | join 37 | end. 38 | join('::') 39 | 40 | "{#{module_name}##{method_name} #{method_name}}" 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/method_details/html/source.erb: -------------------------------------------------------------------------------- 1 | <table class="source_code"> 2 | <tr> 3 | <td class="lines"> 4 | <pre><%= "\n\n\n" %><%= h format_lines(object) %></pre> 5 | </td> 6 | <td class="code"> 7 | <pre><span class="info file"># File '<%= h object.file %>'<% if object.line %>, line <%= object.line %><% end %></span><%= "\n\n" %><%= html_syntax_highlight object.source %></pre> 8 | </td> 9 | </tr> 10 | </table> 11 | -------------------------------------------------------------------------------- /doc_config/yard/templates/default/module/html/box_info.erb: -------------------------------------------------------------------------------- 1 | <% n = 1 %> 2 | <dl class="box"> 3 | <% if CodeObjects::ClassObject === object && object.superclass %> 4 | <dt class="r<%=n%>">Inherits:</dt> 5 | <dd class="r<%=n%>"> 6 | <span class="inheritName"><%= linkify object.superclass %></span> 7 | <% if object.superclass.name != :BasicObject %> 8 | <ul class="fullTree"> 9 | <li><%= linkify P(:Object) %></li> 10 | <% object.inheritance_tree.reverse.each_with_index do |obj, i| %> 11 | <li class="next"><%= obj == object ? obj.path : linkify(obj) %></li> 12 | <% end %> 13 | </ul> 14 | <a href="#" class="inheritanceTree">show all</a> 15 | <% end %> 16 | </dd> 17 | <% n = 2 %> 18 | <% end %> 19 | <% [[:class, "Extended by"], [:instance, "Includes"]].each do |scope, name| %> 20 | <% if (mix = run_verifier(object.mixins(scope))).size > 0 %> 21 | <dt class="r<%=n%>"><%= name %>:</dt> 22 | <dd class="r<%=n%>"><%= mix.sort_by {|o| o.path }.map {|o| linkify(o) }.join(", ") %></dd> 23 | <% n = n == 2 ? 1 : 2 %> 24 | <% end %> 25 | <% end %> 26 | <% if (mixed_into = mixed_into(object)).size > 0 %> 27 | <dt class="r<%=n%>">Included in:</dt> 28 | <dd class="r<%=n%>"><%= mixed_into.sort_by {|o| o.path }.map {|o| linkify(o) }.join(", ") %></dd> 29 | <% n = n == 2 ? 1 : 2 %> 30 | <% end %> 31 | </dl> 32 | -------------------------------------------------------------------------------- /gemfiles/rails_6_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal", "2.5.0" 6 | gem "bundler", "~> 2.0" 7 | gem "pry" 8 | gem "pry-byebug" 9 | gem "rake", "13.0.1" 10 | gem "rspec", "~> 3.9" 11 | gem "rubocop", require: false 12 | gem "rubocop-packaging", require: false 13 | gem "rubocop-rails", require: false 14 | gem "warnings_logger" 15 | gem "zeus", require: false 16 | gem "fssm" 17 | gem "redcarpet" 18 | gem "rouge" 19 | gem "yard" 20 | gem "spring" 21 | gem "spring-watcher-listen", "~> 2.0.0" 22 | gem "rails-controller-testing", ">= 1.0.1" 23 | gem "rails", "6.1.7.7" 24 | gem "puma", "~> 5.0" 25 | gem "sass-rails", ">= 6" 26 | gem "turbolinks", "~> 5" 27 | gem "jbuilder", "~> 2.7" 28 | gem "bcrypt", "~> 3.1.7" 29 | gem "bootsnap", ">= 1.4.4", require: false 30 | gem "rack-mini-profiler", "~> 2.0.0" 31 | gem "listen", "~> 3.3" 32 | gem "capybara", ">= 3.26" 33 | gem "selenium-webdriver", ">= 4.0.0.rc1" 34 | gem "webdrivers" 35 | gem "net-smtp", require: false 36 | gem "psych", "~> 3.0" 37 | gem "rspec-rails", "~> 6.0" 38 | gem "shoulda-context", "~> 2.0.0" 39 | gem "pg", ">= 0.18", "< 2.0" 40 | gem "sqlite3", "~> 1.4" 41 | -------------------------------------------------------------------------------- /gemfiles/rails_7_0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal", "2.5.0" 6 | gem "bundler", "~> 2.0" 7 | gem "pry" 8 | gem "pry-byebug" 9 | gem "rake", "13.0.1" 10 | gem "rspec", "~> 3.9" 11 | gem "rubocop", require: false 12 | gem "rubocop-packaging", require: false 13 | gem "rubocop-rails", require: false 14 | gem "warnings_logger" 15 | gem "zeus", require: false 16 | gem "fssm" 17 | gem "redcarpet" 18 | gem "rouge" 19 | gem "yard" 20 | gem "spring" 21 | gem "spring-watcher-listen", "~> 2.0.0" 22 | gem "rails-controller-testing", ">= 1.0.1" 23 | gem "rails", "7.0.8.1" 24 | gem "sprockets-rails" 25 | gem "puma", "~> 5.0" 26 | gem "importmap-rails" 27 | gem "turbo-rails" 28 | gem "stimulus-rails" 29 | gem "jbuilder" 30 | gem "bootsnap", require: false 31 | gem "capybara" 32 | gem "selenium-webdriver" 33 | gem "webdrivers" 34 | gem "rspec-rails", "~> 6.0" 35 | gem "shoulda-context", "~> 2.0.0" 36 | gem "bcrypt", "~> 3.1.7" 37 | gem "sqlite3", "~> 1.4" 38 | gem "pg", "~> 1.1" 39 | -------------------------------------------------------------------------------- /gemfiles/rails_7_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal", "2.5.0" 6 | gem "bundler", "~> 2.0" 7 | gem "pry" 8 | gem "pry-byebug" 9 | gem "rake", "13.0.1" 10 | gem "rspec", "~> 3.9" 11 | gem "rubocop", require: false 12 | gem "rubocop-packaging", require: false 13 | gem "rubocop-rails", require: false 14 | gem "warnings_logger" 15 | gem "zeus", require: false 16 | gem "fssm" 17 | gem "redcarpet" 18 | gem "rouge" 19 | gem "yard" 20 | gem "spring" 21 | gem "spring-watcher-listen", "~> 2.0.0" 22 | gem "rails-controller-testing", ">= 1.0.1" 23 | gem "rails", "7.1.3.2" 24 | gem "sprockets-rails" 25 | gem "puma", "~> 6.0" 26 | gem "importmap-rails" 27 | gem "turbo-rails" 28 | gem "stimulus-rails" 29 | gem "jbuilder" 30 | gem "bootsnap", require: false 31 | gem "capybara" 32 | gem "selenium-webdriver" 33 | gem "webdrivers" 34 | gem "rspec-rails", "~> 6.0" 35 | gem "shoulda-context", "~> 2.0.0" 36 | gem "bcrypt", "~> 3.1.7" 37 | gem "sqlite3", "~> 1.4" 38 | gem "pg", "~> 1.1" 39 | 40 | if RUBY_VERSION >= "3.1" && RUBY_VERSION < "3.2" 41 | gem "error_highlight", ">= 0.4.0", platforms: [:ruby] 42 | end 43 | 44 | -------------------------------------------------------------------------------- /gemfiles/rails_7_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal", "2.5.0" 6 | gem "bundler", "~> 2.0" 7 | gem "pry" 8 | gem "pry-byebug" 9 | gem "rake", "13.0.1" 10 | gem "rspec", "~> 3.9" 11 | gem "rubocop", require: false 12 | gem "rubocop-packaging", require: false 13 | gem "rubocop-rails", require: false 14 | gem "warnings_logger" 15 | gem "zeus", require: false 16 | gem "fssm" 17 | gem "redcarpet" 18 | gem "rouge" 19 | gem "yard" 20 | gem "spring" 21 | gem "spring-watcher-listen", "~> 2.0.0" 22 | gem "rails-controller-testing", ">= 1.0.1" 23 | gem "rails", "~> 7.2.0" 24 | gem "brakeman", require: false 25 | gem "rubocop-rails-omakase", require: false 26 | gem "sprockets-rails" 27 | gem "puma", "~> 6.0" 28 | gem "importmap-rails" 29 | gem "turbo-rails" 30 | gem "stimulus-rails" 31 | gem "jbuilder" 32 | gem "bootsnap", require: false 33 | gem "capybara" 34 | gem "selenium-webdriver" 35 | gem "webdrivers" 36 | gem "rspec-rails", "~> 6.0" 37 | gem "shoulda-context", "~> 2.0.0" 38 | gem "bcrypt", "~> 3.1.7" 39 | gem "sqlite3", "~> 1.4" 40 | gem "pg", "~> 1.1" 41 | 42 | if RUBY_VERSION >= "3.1" && RUBY_VERSION < "3.2" 43 | gem "error_highlight", ">= 0.4.0", platforms: [:ruby] 44 | end 45 | -------------------------------------------------------------------------------- /lib/shoulda-matchers.rb: -------------------------------------------------------------------------------- 1 | require 'shoulda/matchers' 2 | -------------------------------------------------------------------------------- /lib/shoulda/matchers.rb: -------------------------------------------------------------------------------- 1 | require 'shoulda/matchers/configuration' 2 | require 'shoulda/matchers/doublespeak' 3 | require 'shoulda/matchers/error' 4 | require 'shoulda/matchers/independent' 5 | require 'shoulda/matchers/integrations' 6 | require 'shoulda/matchers/matcher_context' 7 | require 'shoulda/matchers/rails_shim' 8 | require 'shoulda/matchers/util' 9 | require 'shoulda/matchers/version' 10 | require 'shoulda/matchers/warn' 11 | 12 | require 'shoulda/matchers/action_controller' 13 | require 'shoulda/matchers/active_model' 14 | require 'shoulda/matchers/active_record' 15 | require 'shoulda/matchers/routing' 16 | 17 | module Shoulda # :nodoc: 18 | module Matchers # :nodoc: 19 | class << self 20 | # @private 21 | attr_accessor :assertion_exception_class 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/action_controller.rb: -------------------------------------------------------------------------------- 1 | require 'shoulda/matchers/action_controller/filter_param_matcher' 2 | require 'shoulda/matchers/action_controller/route_params' 3 | require 'shoulda/matchers/action_controller/set_flash_matcher' 4 | require 'shoulda/matchers/action_controller/render_with_layout_matcher' 5 | require 'shoulda/matchers/action_controller/respond_with_matcher' 6 | require 'shoulda/matchers/action_controller/set_session_matcher' 7 | require 'shoulda/matchers/action_controller/route_matcher' 8 | require 'shoulda/matchers/action_controller/redirect_to_matcher' 9 | require 'shoulda/matchers/action_controller/render_template_matcher' 10 | require 'shoulda/matchers/action_controller/rescue_from_matcher' 11 | require 'shoulda/matchers/action_controller/callback_matcher' 12 | require 'shoulda/matchers/action_controller/permit_matcher' 13 | require 'shoulda/matchers/action_controller/set_session_or_flash_matcher' 14 | require 'shoulda/matchers/action_controller/flash_store' 15 | require 'shoulda/matchers/action_controller/session_store' 16 | 17 | module Shoulda 18 | module Matchers 19 | # This module provides matchers that are used to test behavior within 20 | # controllers. 21 | module ActionController 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/action_controller/filter_param_matcher.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActionController 4 | # The `filter_param` matcher is used to test parameter filtering 5 | # configuration. Specifically, it asserts that the given parameter is 6 | # present in `config.filter_parameters`. 7 | # 8 | # class MyApplication < Rails::Application 9 | # config.filter_parameters << :secret_key 10 | # end 11 | # 12 | # # RSpec 13 | # RSpec.describe ApplicationController, type: :controller do 14 | # it { should filter_param(:secret_key) } 15 | # end 16 | # 17 | # # Minitest (Shoulda) 18 | # class ApplicationControllerTest < ActionController::TestCase 19 | # should filter_param(:secret_key) 20 | # end 21 | # 22 | # @return [FilterParamMatcher] 23 | # 24 | def filter_param(key) 25 | FilterParamMatcher.new(key) 26 | end 27 | 28 | # @private 29 | class FilterParamMatcher 30 | def initialize(key) 31 | @key = key 32 | end 33 | 34 | def matches?(_controller) 35 | filters_key? 36 | end 37 | 38 | def failure_message 39 | "Expected #{@key} to be filtered; filtered keys:"\ 40 | " #{filtered_keys.join(', ')}" 41 | end 42 | 43 | def failure_message_when_negated 44 | "Did not expect #{@key} to be filtered" 45 | end 46 | 47 | def description 48 | "filter #{@key}" 49 | end 50 | 51 | private 52 | 53 | def filters_key? 54 | filtered_keys.any? do |filter| 55 | case filter 56 | when Regexp 57 | filter =~ @key 58 | else 59 | filter == @key 60 | end 61 | end 62 | end 63 | 64 | def filtered_keys 65 | Rails.application.config.filter_parameters 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/action_controller/flash_store.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/module/delegation' 2 | module Shoulda 3 | module Matchers 4 | module ActionController 5 | # @private 6 | class FlashStore 7 | def self.future 8 | new 9 | end 10 | 11 | def self.now 12 | new.use_now! 13 | end 14 | 15 | attr_accessor :controller 16 | 17 | def initialize 18 | @use_now = false 19 | end 20 | 21 | def name 22 | if @use_now 23 | 'flash.now' 24 | else 25 | 'flash' 26 | end 27 | end 28 | 29 | def has_key?(key) 30 | values_to_check.include?(key.to_s) 31 | end 32 | 33 | def has_value?(expected_value) 34 | values_to_check.values.any? do |actual_value| 35 | expected_value === actual_value 36 | end 37 | end 38 | delegate :empty?, to: :flash 39 | 40 | def use_now! 41 | @use_now = true 42 | self 43 | end 44 | 45 | private 46 | 47 | def flash 48 | @_flash ||= copy_of_flash_from_controller 49 | end 50 | 51 | def copy_of_flash_from_controller 52 | controller.flash.dup.tap do |flash| 53 | copy_flashes(controller.flash, flash) 54 | copy_discard_if_necessary(controller.flash, flash) 55 | end 56 | end 57 | 58 | def copy_flashes(original_flash, new_flash) 59 | flashes = original_flash.instance_variable_get('@flashes').dup 60 | new_flash.instance_variable_set('@flashes', flashes) 61 | end 62 | 63 | def copy_discard_if_necessary(original_flash, new_flash) 64 | discard = original_flash.instance_variable_get('@discard').dup 65 | new_flash.instance_variable_set('@discard', discard) 66 | end 67 | 68 | def set_values 69 | flash.instance_variable_get('@flashes') 70 | end 71 | 72 | def keys_to_discard 73 | flash.instance_variable_get('@discard') 74 | end 75 | 76 | def values_to_check 77 | if @use_now 78 | set_values.slice(*keys_to_discard.to_a) 79 | else 80 | set_values.except(*keys_to_discard.to_a) 81 | end 82 | end 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/action_controller/route_params.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActionController 4 | # @private 5 | class RouteParams 6 | PARAMS_TO_SYMBOLIZE = %i{format}.freeze 7 | 8 | def initialize(args) 9 | @args = args 10 | end 11 | 12 | def normalize 13 | if controller_and_action_given_as_string? 14 | extract_params_from_string 15 | else 16 | stringify_params 17 | end 18 | end 19 | 20 | protected 21 | 22 | attr_reader :args 23 | 24 | def controller_and_action_given_as_string? 25 | args[0].is_a?(String) 26 | end 27 | 28 | def extract_params_from_string 29 | controller, action = args[0].split('#') 30 | params = (args[1] || {}).merge!(controller: controller, action: action) 31 | normalize_values(params) 32 | end 33 | 34 | def stringify_params 35 | normalize_values(args[0]) 36 | end 37 | 38 | def normalize_values(hash) 39 | hash.each_with_object({}) do |(key, value), hash_copy| 40 | hash_copy[key] = symbolize_or_stringify(key, value) 41 | end 42 | end 43 | 44 | def symbolize_or_stringify(key, value) 45 | if PARAMS_TO_SYMBOLIZE.include?(key) 46 | value.to_sym 47 | else 48 | stringify(value) 49 | end 50 | end 51 | 52 | def stringify(value) 53 | if value.is_a?(Array) 54 | value.map(&:to_param) 55 | else 56 | value.to_param 57 | end 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/action_controller/session_store.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActionController 4 | # @private 5 | class SessionStore 6 | attr_accessor :controller 7 | 8 | def name 9 | 'session' 10 | end 11 | 12 | def has_key?(key) 13 | session.key?(key) 14 | end 15 | 16 | def has_value?(expected_value) 17 | session.values.any? do |actual_value| 18 | expected_value === actual_value 19 | end 20 | end 21 | 22 | def empty? 23 | session.empty? 24 | end 25 | 26 | private 27 | 28 | def session 29 | controller.session 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/allow_value_matcher/attribute_changed_value_error.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | class AllowValueMatcher 5 | # @private 6 | class AttributeChangedValueError < Shoulda::Matchers::Error 7 | attr_accessor :matcher_name, :model, :attribute_name, :value_written, 8 | :value_read 9 | 10 | def message 11 | Shoulda::Matchers.word_wrap <<-MESSAGE 12 | The #{matcher_name} matcher attempted to set :#{attribute_name} on 13 | #{model.name} to #{value_written.inspect}, but when the attribute was 14 | read back, it had stored #{value_read.inspect} instead. 15 | 16 | This creates a problem because it means that the model is behaving in a 17 | way that is interfering with the test -- there's a mismatch between the 18 | test that you wrote and test that we actually ran. 19 | 20 | There are a couple of reasons why this could be happening: 21 | 22 | * ActiveRecord is typecasting the incoming value. 23 | * The writer method for :#{attribute_name} has been overridden so that 24 | incoming values are changed in some way. 25 | 26 | If this exception makes sense to you and you wish to bypass it, try 27 | adding the `ignoring_interference_by_writer` qualifier onto the end of 28 | your matcher. If the test still does not pass after that, then you may 29 | need to do something different. 30 | 31 | If you need help, feel free to ask a question on the shoulda-matchers 32 | issues list: 33 | 34 | https://github.com/thoughtbot/shoulda-matchers/issues 35 | MESSAGE 36 | end 37 | 38 | def successful? 39 | false 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/allow_value_matcher/attribute_does_not_exist_error.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | class AllowValueMatcher 5 | # @private 6 | class AttributeDoesNotExistError < Shoulda::Matchers::Error 7 | attr_accessor :model, :attribute_name, :value 8 | 9 | def message 10 | Shoulda::Matchers.word_wrap <<-MESSAGE 11 | The matcher attempted to set :#{attribute_name} on the #{model.name} to 12 | #{value.inspect}, but that attribute does not exist. 13 | MESSAGE 14 | end 15 | 16 | def successful? 17 | false 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter_and_validator.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Shoulda 4 | module Matchers 5 | module ActiveModel 6 | class AllowValueMatcher 7 | # @private 8 | class AttributeSetterAndValidator 9 | extend Forwardable 10 | 11 | def_delegators( 12 | :allow_value_matcher, 13 | :after_setting_value_callback, 14 | :attribute_to_check_message_against, 15 | :context, 16 | :expected_message, 17 | :expects_strict?, 18 | :ignore_interference_by_writer, 19 | :instance, 20 | ) 21 | 22 | def initialize(allow_value_matcher, attribute_name, value) 23 | @allow_value_matcher = allow_value_matcher 24 | @attribute_name = attribute_name 25 | @value = value 26 | @_attribute_setter = nil 27 | @_validator = nil 28 | end 29 | 30 | def attribute_setter 31 | @_attribute_setter ||= AttributeSetter.new( 32 | matcher_name: :allow_value, 33 | object: instance, 34 | attribute_name: attribute_name, 35 | value: value, 36 | ignore_interference_by_writer: ignore_interference_by_writer, 37 | after_set_callback: after_setting_value_callback, 38 | ) 39 | end 40 | 41 | def attribute_setter_description 42 | attribute_setter.description 43 | end 44 | 45 | def validator 46 | @_validator ||= Validator.new( 47 | instance, 48 | attribute_to_check_message_against, 49 | context: context, 50 | expects_strict: expects_strict?, 51 | expected_message: expected_message, 52 | ) 53 | end 54 | 55 | protected 56 | 57 | attr_reader :allow_value_matcher, :attribute_name, :value 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | class AllowValueMatcher 5 | # @private 6 | class AttributeSetters 7 | include Enumerable 8 | 9 | def initialize(allow_value_matcher, values) 10 | @tuples = values.map do |attribute_name, value| 11 | AttributeSetterAndValidator.new( 12 | allow_value_matcher, 13 | attribute_name, 14 | value, 15 | ) 16 | end 17 | end 18 | 19 | def each(&block) 20 | tuples.each(&block) 21 | end 22 | 23 | def first_failing 24 | tuples.detect(&method(:does_not_match?)) 25 | end 26 | 27 | protected 28 | 29 | attr_reader :tuples 30 | 31 | private 32 | 33 | def does_not_match?(tuple) 34 | !tuple.attribute_setter.set! 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setters_and_validators.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | class AllowValueMatcher 5 | # @private 6 | class AttributeSettersAndValidators 7 | include Enumerable 8 | 9 | def initialize(allow_value_matcher, values) 10 | @tuples = values.map do |attribute_name, value| 11 | AttributeSetterAndValidator.new( 12 | allow_value_matcher, 13 | attribute_name, 14 | value, 15 | ) 16 | end 17 | end 18 | 19 | def each(&block) 20 | tuples.each(&block) 21 | end 22 | 23 | def first_passing 24 | tuples.detect(&method(:matches?)) 25 | end 26 | 27 | def first_failing 28 | tuples.detect(&method(:does_not_match?)) 29 | end 30 | 31 | protected 32 | 33 | attr_reader :tuples 34 | 35 | private 36 | 37 | def matches?(tuple) 38 | tuple.attribute_setter.set! && tuple.validator.call 39 | end 40 | 41 | def does_not_match?(tuple) 42 | !matches?(tuple) 43 | end 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/allow_value_matcher/successful_check.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | class AllowValueMatcher 5 | # @private 6 | class SuccessfulCheck 7 | def successful? 8 | true 9 | end 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/allow_value_matcher/successful_setting.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | class AllowValueMatcher 5 | # @private 6 | class SuccessfulSetting 7 | def successful? 8 | true 9 | end 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/disallow_value_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Shoulda 4 | module Matchers 5 | module ActiveModel 6 | # @private 7 | class DisallowValueMatcher 8 | extend Forwardable 9 | 10 | def_delegators( 11 | :allow_matcher, 12 | :_after_setting_value, 13 | :attribute_changed_value_message=, 14 | :attribute_to_set, 15 | :description, 16 | :expects_strict?, 17 | :failure_message_preface, 18 | :failure_message_preface=, 19 | :ignore_interference_by_writer, 20 | :last_attribute_setter_used, 21 | :last_value_set, 22 | :model, 23 | :simple_description, 24 | :values_to_preset=, 25 | ) 26 | 27 | def initialize(value) 28 | @allow_matcher = AllowValueMatcher.new(value) 29 | end 30 | 31 | def matches?(subject) 32 | allow_matcher.does_not_match?(subject) 33 | end 34 | 35 | def does_not_match?(subject) 36 | allow_matcher.matches?(subject) 37 | end 38 | 39 | def for(attribute) 40 | allow_matcher.for(attribute) 41 | self 42 | end 43 | 44 | def on(context) 45 | allow_matcher.on(context) 46 | self 47 | end 48 | 49 | def with_message(message, options = {}) 50 | allow_matcher.with_message(message, options) 51 | self 52 | end 53 | 54 | def strict(strict = true) 55 | allow_matcher.strict(strict) 56 | self 57 | end 58 | 59 | def ignoring_interference_by_writer(value = :always) 60 | allow_matcher.ignoring_interference_by_writer(value) 61 | self 62 | end 63 | 64 | def failure_message 65 | allow_matcher.failure_message_when_negated 66 | end 67 | 68 | def failure_message_when_negated 69 | allow_matcher.failure_message 70 | end 71 | 72 | protected 73 | 74 | attr_reader :allow_matcher 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/errors.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | # @private 5 | class CouldNotDetermineValueOutsideOfArray < RuntimeError; end 6 | 7 | # @private 8 | class NonNullableBooleanError < Shoulda::Matchers::Error 9 | def self.create(attribute) 10 | super(attribute: attribute) 11 | end 12 | 13 | attr_accessor :attribute 14 | 15 | def message 16 | <<-EOT.strip 17 | You have specified that your model's #{attribute} should ensure inclusion of nil. 18 | However, #{attribute} is a boolean column which does not allow null values. 19 | Hence, this test will fail and there is no way to make it pass. 20 | EOT 21 | end 22 | end 23 | 24 | # @private 25 | class CouldNotSetPasswordError < Shoulda::Matchers::Error 26 | def self.create(model) 27 | super(model: model) 28 | end 29 | 30 | attr_accessor :model 31 | 32 | def message 33 | <<-EOT.strip 34 | The validation failed because your #{model_name} model declares `has_secure_password`, and 35 | `validate_presence_of` was called on a #{record_name} which has `password` already set to a value. 36 | Please use a #{record_name} with an empty `password` instead. 37 | EOT 38 | end 39 | 40 | private 41 | 42 | def model_name 43 | model.name 44 | end 45 | 46 | def record_name 47 | model_name.humanize.downcase 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/helpers.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | # @private 5 | module Helpers 6 | def pretty_error_messages(object) 7 | format_validation_errors(object.errors) 8 | end 9 | 10 | def format_validation_errors(errors) 11 | list_items = errors.to_hash.keys.map do |attribute| 12 | messages = errors[attribute] 13 | "* #{attribute}: #{messages}" 14 | end 15 | 16 | list_items.join("\n") 17 | end 18 | 19 | def default_error_message(type, options = {}) 20 | model_name = options.delete(:model_name) 21 | attribute = options.delete(:attribute) 22 | instance = options.delete(:instance) 23 | 24 | RailsShim.generate_validation_message( 25 | instance, 26 | attribute.to_sym, 27 | type, 28 | model_name, 29 | options, 30 | ) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/numericality_matchers.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | # @private 5 | module NumericalityMatchers 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/numericality_matchers/even_number_matcher.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | module NumericalityMatchers 5 | # @private 6 | class EvenNumberMatcher < NumericTypeMatcher 7 | NON_EVEN_NUMBER_VALUE = 1 8 | 9 | def simple_description 10 | description = '' 11 | 12 | if expects_strict? 13 | description << 'strictly ' 14 | end 15 | 16 | description + 17 | "disallow :#{attribute} from being an odd number" 18 | end 19 | 20 | def allowed_type_adjective 21 | 'even' 22 | end 23 | 24 | def diff_to_compare 25 | 2 26 | end 27 | 28 | protected 29 | 30 | def wrap_disallow_value_matcher(matcher) 31 | matcher.with_message(:even) 32 | end 33 | 34 | def disallowed_value 35 | if @numeric_type_matcher.given_numeric_column? 36 | NON_EVEN_NUMBER_VALUE 37 | else 38 | NON_EVEN_NUMBER_VALUE.to_s 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Shoulda 4 | module Matchers 5 | module ActiveModel 6 | module NumericalityMatchers 7 | # @private 8 | class NumericTypeMatcher 9 | extend Forwardable 10 | 11 | def_delegators( 12 | :disallow_value_matcher, 13 | :expects_custom_validation_message?, 14 | :expects_strict?, 15 | :failure_message, 16 | :failure_message_when_negated, 17 | :ignore_interference_by_writer, 18 | :ignoring_interference_by_writer, 19 | :matches?, 20 | :does_not_match?, 21 | :on, 22 | :strict, 23 | :with_message, 24 | ) 25 | 26 | def initialize(numeric_type_matcher, attribute) 27 | @numeric_type_matcher = numeric_type_matcher 28 | @attribute = attribute 29 | end 30 | 31 | def allowed_type_name 32 | 'number' 33 | end 34 | 35 | def allowed_type_adjective 36 | '' 37 | end 38 | 39 | def diff_to_compare 40 | raise NotImplementedError 41 | end 42 | 43 | protected 44 | 45 | attr_reader :attribute 46 | 47 | def wrap_disallow_value_matcher(_matcher) 48 | raise NotImplementedError 49 | end 50 | 51 | def disallowed_value 52 | raise NotImplementedError 53 | end 54 | 55 | private 56 | 57 | def disallow_value_matcher 58 | @_disallow_value_matcher ||= DisallowValueMatcher.new(disallowed_value).tap do |matcher| 59 | matcher.for(attribute) 60 | wrap_disallow_value_matcher(matcher) 61 | end 62 | end 63 | end 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/numericality_matchers/odd_number_matcher.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | module NumericalityMatchers 5 | # @private 6 | class OddNumberMatcher < NumericTypeMatcher 7 | NON_ODD_NUMBER_VALUE = 2 8 | 9 | def simple_description 10 | description = '' 11 | 12 | if expects_strict? 13 | description << 'strictly ' 14 | end 15 | 16 | description + 17 | "disallow :#{attribute} from being an even number" 18 | end 19 | 20 | def allowed_type_adjective 21 | 'odd' 22 | end 23 | 24 | def diff_to_compare 25 | 2 26 | end 27 | 28 | protected 29 | 30 | def wrap_disallow_value_matcher(matcher) 31 | matcher.with_message(:odd) 32 | end 33 | 34 | def disallowed_value 35 | if @numeric_type_matcher.given_numeric_column? 36 | NON_ODD_NUMBER_VALUE 37 | else 38 | NON_ODD_NUMBER_VALUE.to_s 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/numericality_matchers/only_integer_matcher.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | module NumericalityMatchers 5 | # @private 6 | class OnlyIntegerMatcher < NumericTypeMatcher 7 | NON_INTEGER_VALUE = 0.1 8 | 9 | def simple_description 10 | description = '' 11 | 12 | if expects_strict? 13 | description << ' strictly' 14 | end 15 | 16 | description + "disallow :#{attribute} from being a decimal number" 17 | end 18 | 19 | def allowed_type_name 20 | 'integer' 21 | end 22 | 23 | def diff_to_compare 24 | 1 25 | end 26 | 27 | protected 28 | 29 | def wrap_disallow_value_matcher(matcher) 30 | matcher.with_message(:not_an_integer) 31 | end 32 | 33 | def disallowed_value 34 | if @numeric_type_matcher.given_numeric_column? 35 | NON_INTEGER_VALUE 36 | else 37 | NON_INTEGER_VALUE.to_s 38 | end 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/numericality_matchers/range_matcher.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/module/delegation' 2 | 3 | module Shoulda 4 | module Matchers 5 | module ActiveModel 6 | module NumericalityMatchers 7 | # @private 8 | class RangeMatcher < ValidationMatcher 9 | OPERATORS = [:>=, :<=].freeze 10 | 11 | delegate :failure_message, to: :submatchers 12 | 13 | def initialize(numericality_matcher, attribute, range) 14 | super(attribute) 15 | unless numericality_matcher.respond_to? :diff_to_compare 16 | raise ArgumentError, 'numericality_matcher is invalid' 17 | end 18 | 19 | @numericality_matcher = numericality_matcher 20 | @range = range 21 | @attribute = attribute 22 | end 23 | 24 | def matches?(subject) 25 | @subject = subject 26 | submatchers.matches?(subject) 27 | end 28 | 29 | def simple_description 30 | description = '' 31 | 32 | if expects_strict? 33 | description << ' strictly' 34 | end 35 | 36 | description + 37 | "disallow :#{attribute} from being a number that is not " + 38 | range_description 39 | end 40 | 41 | def range_description 42 | "from #{Shoulda::Matchers::Util.inspect_range(@range)}" 43 | end 44 | 45 | def submatchers 46 | @_submatchers ||= NumericalityMatchers::Submatchers.new(build_submatchers) 47 | end 48 | 49 | private 50 | 51 | def build_submatchers 52 | submatcher_combos.map do |value, operator| 53 | build_comparison_submatcher(value, operator) 54 | end 55 | end 56 | 57 | def submatcher_combos 58 | @range.minmax.zip(OPERATORS) 59 | end 60 | 61 | def build_comparison_submatcher(value, operator) 62 | ComparisonMatcher.new(@numericality_matcher, value, operator). 63 | for(@attribute). 64 | with_message(@message). 65 | on(@context) 66 | end 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | module NumericalityMatchers 5 | # @private 6 | class Submatchers 7 | def initialize(submatchers) 8 | @submatchers = submatchers 9 | end 10 | 11 | def matches?(subject) 12 | @subject = subject 13 | failing_submatchers.empty? 14 | end 15 | 16 | def failure_message 17 | failing_submatcher.failure_message 18 | end 19 | 20 | def failure_message_when_negated 21 | non_failing_submatcher.failure_message_when_negated 22 | end 23 | 24 | def add(submatcher) 25 | @submatchers << submatcher 26 | end 27 | 28 | private 29 | 30 | def failing_submatchers 31 | @_failing_submatchers ||= @submatchers.reject do |submatcher| 32 | submatcher.matches?(@subject) 33 | end 34 | end 35 | 36 | def non_failing_submatchers 37 | @_non_failing_submatchers ||= @submatchers.reject do |submatcher| 38 | submatcher.does_not_match?(@subject) 39 | end 40 | end 41 | 42 | def failing_submatcher 43 | failing_submatchers.last 44 | end 45 | 46 | def non_failing_submatcher 47 | non_failing_submatchers.last 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/qualifiers.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | # @private 5 | module Qualifiers 6 | end 7 | end 8 | end 9 | end 10 | 11 | require_relative 'qualifiers/allow_nil' 12 | require_relative 'qualifiers/allow_blank' 13 | require_relative 'qualifiers/ignore_interference_by_writer' 14 | require_relative 'qualifiers/ignoring_interference_by_writer' 15 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/qualifiers/allow_blank.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | module Qualifiers 5 | # @private 6 | module AllowBlank 7 | def initialize(*args) 8 | super 9 | @expects_to_allow_blank = false 10 | end 11 | 12 | def allow_blank 13 | @expects_to_allow_blank = true 14 | self 15 | end 16 | 17 | protected 18 | 19 | def expects_to_allow_blank? 20 | @expects_to_allow_blank 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/qualifiers/allow_nil.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | module Qualifiers 5 | # @private 6 | module AllowNil 7 | def initialize(*args) 8 | super 9 | @expects_to_allow_nil = false 10 | end 11 | 12 | def allow_nil 13 | @expects_to_allow_nil = true 14 | self 15 | end 16 | 17 | protected 18 | 19 | def expects_to_allow_nil? 20 | @expects_to_allow_nil 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/qualifiers/ignoring_interference_by_writer.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | module Qualifiers 5 | # @private 6 | module IgnoringInterferenceByWriter 7 | attr_reader :ignore_interference_by_writer 8 | 9 | def initialize(*) 10 | @ignore_interference_by_writer = IgnoreInterferenceByWriter.new 11 | end 12 | 13 | def ignoring_interference_by_writer(value = :always) 14 | @ignore_interference_by_writer.set(value) 15 | self 16 | end 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/validation_matcher/build_description.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | class ValidationMatcher 5 | # @private 6 | class BuildDescription 7 | def self.call(matcher, main_description) 8 | new(matcher, main_description).call 9 | end 10 | 11 | def initialize(matcher, main_description) 12 | @matcher = matcher 13 | @main_description = main_description 14 | end 15 | 16 | def call 17 | if description_clauses_for_qualifiers.any? 18 | "#{main_description}#{clause_for_allow_blank_or_nil},"\ 19 | " #{description_clauses_for_qualifiers.to_sentence}" 20 | else 21 | main_description + clause_for_allow_blank_or_nil 22 | end 23 | end 24 | 25 | protected 26 | 27 | attr_reader :matcher, :main_description 28 | 29 | private 30 | 31 | def clause_for_allow_blank_or_nil 32 | if matcher.try(:expects_to_allow_blank?) 33 | ' as long as it is not blank' 34 | elsif matcher.try(:expects_to_allow_nil?) 35 | ' as long as it is not nil' 36 | else 37 | '' 38 | end 39 | end 40 | 41 | def description_clauses_for_qualifiers 42 | description_clauses = [] 43 | 44 | if matcher.try(:expects_strict?) 45 | description_clauses << 46 | if matcher.try(:expects_custom_validation_message?) 47 | 'raising a validation exception with a custom message on failure' 48 | else 49 | 'raising a validation exception on failure' 50 | end 51 | elsif matcher.try(:expects_custom_validation_message?) 52 | description_clauses << 53 | 'producing a custom validation error on failure' 54 | end 55 | 56 | description_clauses 57 | end 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_model/validation_message_finder.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveModel 4 | # @private 5 | class ValidationMessageFinder 6 | include Helpers 7 | 8 | def initialize(instance, attribute, context = nil) 9 | @instance = instance 10 | @attribute = attribute 11 | @context = context 12 | end 13 | 14 | def allow_description(allowed_values) 15 | "allow #{@attribute} to be set to #{allowed_values}" 16 | end 17 | 18 | def expected_message_from(attribute_message) 19 | attribute_message 20 | end 21 | 22 | def has_messages? 23 | errors.present? 24 | end 25 | 26 | def source_description 27 | 'errors' 28 | end 29 | 30 | def messages_description 31 | if errors.empty? 32 | ' no errors' 33 | else 34 | " errors:\n#{pretty_error_messages(validated_instance)}" 35 | end 36 | end 37 | 38 | def messages 39 | Array(messages_for_attribute) 40 | end 41 | 42 | private 43 | 44 | def messages_for_attribute 45 | errors[@attribute] 46 | end 47 | 48 | def errors 49 | validated_instance.errors 50 | end 51 | 52 | def validated_instance 53 | @_validated_instance ||= validate_instance 54 | end 55 | 56 | def validate_instance 57 | @instance.valid?(*@context) 58 | @instance 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record.rb: -------------------------------------------------------------------------------- 1 | require 'shoulda/matchers/active_record/association_matcher' 2 | require 'shoulda/matchers/active_record/association_matchers' 3 | require 'shoulda/matchers/active_record/association_matchers/counter_cache_matcher' 4 | require 'shoulda/matchers/active_record/association_matchers/inverse_of_matcher' 5 | require 'shoulda/matchers/active_record/association_matchers/join_table_matcher' 6 | require 'shoulda/matchers/active_record/association_matchers/order_matcher' 7 | require 'shoulda/matchers/active_record/association_matchers/through_matcher' 8 | require 'shoulda/matchers/active_record/association_matchers/dependent_matcher' 9 | require 'shoulda/matchers/active_record/association_matchers/required_matcher' 10 | require 'shoulda/matchers/active_record/association_matchers/optional_matcher' 11 | require 'shoulda/matchers/active_record/association_matchers/source_matcher' 12 | require 'shoulda/matchers/active_record/association_matchers/model_reflector' 13 | require 'shoulda/matchers/active_record/association_matchers/model_reflection' 14 | require 'shoulda/matchers/active_record/association_matchers/option_verifier' 15 | require 'shoulda/matchers/active_record/have_db_column_matcher' 16 | require 'shoulda/matchers/active_record/have_db_index_matcher' 17 | require 'shoulda/matchers/active_record/have_implicit_order_column' 18 | require 'shoulda/matchers/active_record/have_readonly_attribute_matcher' 19 | require 'shoulda/matchers/active_record/have_rich_text_matcher' 20 | require 'shoulda/matchers/active_record/have_secure_token_matcher' 21 | require 'shoulda/matchers/active_record/serialize_matcher' 22 | require 'shoulda/matchers/active_record/accept_nested_attributes_for_matcher' 23 | require 'shoulda/matchers/active_record/define_enum_for_matcher' 24 | require 'shoulda/matchers/active_record/uniqueness' 25 | require 'shoulda/matchers/active_record/validate_uniqueness_of_matcher' 26 | require 'shoulda/matchers/active_record/have_attached_matcher' 27 | require 'shoulda/matchers/active_record/normalize_matcher' 28 | require 'shoulda/matchers/active_record/encrypt_matcher' 29 | 30 | module Shoulda 31 | module Matchers 32 | # This module provides matchers that are used to test behavior within 33 | # ActiveRecord classes. 34 | module ActiveRecord 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/association_matchers.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | # @private 5 | module AssociationMatchers 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | module AssociationMatchers 5 | # @private 6 | class CounterCacheMatcher 7 | attr_accessor :missing_option 8 | 9 | def initialize(counter_cache, name) 10 | @counter_cache = counter_cache 11 | @name = name 12 | @missing_option = '' 13 | end 14 | 15 | def description 16 | "counter_cache => #{counter_cache}" 17 | end 18 | 19 | def matches?(subject) 20 | self.subject = ModelReflector.new(subject, name) 21 | 22 | if correct_value? 23 | true 24 | else 25 | self.missing_option = "#{name} should have #{description}" 26 | false 27 | end 28 | end 29 | 30 | protected 31 | 32 | attr_accessor :subject, :counter_cache, :name 33 | 34 | def correct_value? 35 | expected = normalize_value 36 | 37 | if expected.is_a?(Hash) 38 | option_verifier.correct_for_hash?( 39 | :counter_cache, 40 | expected, 41 | ) 42 | else 43 | option_verifier.correct_for_string?( 44 | :counter_cache, 45 | expected, 46 | ) 47 | end 48 | end 49 | 50 | def option_verifier 51 | @_option_verifier ||= OptionVerifier.new(subject) 52 | end 53 | 54 | def normalize_value 55 | if Rails::VERSION::STRING >= '7.2' 56 | case counter_cache 57 | when true 58 | { active: true, column: nil } 59 | when String, Symbol 60 | { active: true, column: counter_cache.to_s } 61 | when Hash 62 | { active: true, column: nil }.merge!(counter_cache) 63 | else 64 | raise ArgumentError, 'Invalid counter_cache option' 65 | end 66 | else 67 | counter_cache 68 | end 69 | end 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | module AssociationMatchers 5 | # @private 6 | class DependentMatcher 7 | attr_accessor :missing_option 8 | 9 | def initialize(dependent, name) 10 | @dependent = dependent 11 | @name = name 12 | @missing_option = '' 13 | end 14 | 15 | def description 16 | "dependent => #{dependent}" 17 | end 18 | 19 | def matches?(subject) 20 | self.subject = ModelReflector.new(subject, name) 21 | 22 | if option_matches? 23 | true 24 | else 25 | self.missing_option = generate_missing_option 26 | false 27 | end 28 | end 29 | 30 | protected 31 | 32 | attr_accessor :subject, :dependent, :name 33 | 34 | private 35 | 36 | def option_verifier 37 | @_option_verifier ||= OptionVerifier.new(subject) 38 | end 39 | 40 | def option_matches? 41 | option_verifier.correct_for?(option_type, :dependent, dependent) 42 | end 43 | 44 | def option_type 45 | case dependent 46 | when true, false then :boolean 47 | else :string 48 | end 49 | end 50 | 51 | def generate_missing_option 52 | [ 53 | "#{name} should have", 54 | (dependent == true ? 'a' : dependent), 55 | 'dependency', 56 | ].join(' ') 57 | end 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/association_matchers/inverse_of_matcher.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | module AssociationMatchers 5 | # @private 6 | class InverseOfMatcher 7 | attr_accessor :missing_option 8 | 9 | def initialize(inverse_of, name) 10 | @inverse_of = inverse_of 11 | @name = name 12 | @missing_option = '' 13 | end 14 | 15 | def description 16 | "inverse_of => #{inverse_of}" 17 | end 18 | 19 | def matches?(subject) 20 | self.subject = ModelReflector.new(subject, name) 21 | 22 | if option_verifier.correct_for_string?(:inverse_of, inverse_of) 23 | true 24 | else 25 | self.missing_option = "#{name} should have #{description}" 26 | false 27 | end 28 | end 29 | 30 | protected 31 | 32 | attr_accessor :subject, :inverse_of, :name 33 | 34 | def option_verifier 35 | @_option_verifier ||= OptionVerifier.new(subject) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/association_matchers/optional_matcher.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | module AssociationMatchers 5 | # @private 6 | class OptionalMatcher 7 | attr_reader :missing_option 8 | 9 | def initialize(attribute_name, optional) 10 | @attribute_name = attribute_name 11 | @optional = optional 12 | @submatcher = ActiveModel::AllowValueMatcher.new(nil). 13 | for(attribute_name) 14 | @missing_option = '' 15 | end 16 | 17 | def description 18 | "optional: #{optional}" 19 | end 20 | 21 | def matches?(subject) 22 | if submatcher_passes?(subject) 23 | true 24 | else 25 | @missing_option = build_missing_option 26 | 27 | false 28 | end 29 | end 30 | 31 | private 32 | 33 | attr_reader :attribute_name, :optional, :submatcher 34 | 35 | def submatcher_passes?(subject) 36 | if optional 37 | submatcher.matches?(subject) 38 | else 39 | submatcher.does_not_match?(subject) 40 | end 41 | end 42 | 43 | def build_missing_option 44 | String.new('and for the record ').tap do |missing_option_string| 45 | missing_option_string << 46 | if optional 47 | 'not to ' 48 | else 49 | 'to ' 50 | end 51 | 52 | missing_option_string << ( 53 | 'fail validation if '\ 54 | ":#{attribute_name} is unset; i.e., either the association "\ 55 | 'should have been defined with `optional: '\ 56 | "#{optional.inspect}`, or there " 57 | ) 58 | 59 | missing_option_string << 60 | if optional 61 | 'should not ' 62 | else 63 | 'should ' 64 | end 65 | 66 | missing_option_string << "be a presence validation on :#{attribute_name}" 67 | end 68 | end 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | module AssociationMatchers 5 | # @private 6 | class OrderMatcher 7 | attr_accessor :missing_option 8 | 9 | def initialize(order, name) 10 | @order = order 11 | @name = name 12 | @missing_option = '' 13 | end 14 | 15 | def description 16 | "order => #{order}" 17 | end 18 | 19 | def matches?(subject) 20 | self.subject = ModelReflector.new(subject, name) 21 | 22 | if option_verifier.correct_for_relation_clause?(:order, order) 23 | true 24 | else 25 | self.missing_option = "#{name} should be ordered by #{order}" 26 | false 27 | end 28 | end 29 | 30 | protected 31 | 32 | attr_accessor :subject, :order, :name 33 | 34 | def option_verifier 35 | @_option_verifier ||= OptionVerifier.new(subject) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/association_matchers/required_matcher.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | module AssociationMatchers 5 | # @private 6 | class RequiredMatcher 7 | attr_reader :missing_option 8 | 9 | def initialize(attribute_name, required) 10 | @attribute_name = attribute_name 11 | @required = required 12 | @submatcher = ActiveModel::DisallowValueMatcher.new(nil). 13 | for(attribute_name). 14 | with_message(validation_message_key) 15 | @missing_option = '' 16 | end 17 | 18 | def description 19 | "required: #{required}" 20 | end 21 | 22 | def matches?(subject) 23 | if submatcher_passes?(subject) 24 | true 25 | else 26 | @missing_option = build_missing_option 27 | 28 | false 29 | end 30 | end 31 | 32 | private 33 | 34 | attr_reader :attribute_name, :required, :submatcher 35 | 36 | def submatcher_passes?(subject) 37 | if required 38 | submatcher.matches?(subject) 39 | else 40 | submatcher.does_not_match?(subject) 41 | end 42 | end 43 | 44 | def validation_message_key 45 | :required 46 | end 47 | 48 | def build_missing_option 49 | String.new('and for the record ').tap do |missing_option_string| 50 | missing_option_string << 51 | if required 52 | 'to ' 53 | else 54 | 'not to ' 55 | end 56 | 57 | missing_option_string << ( 58 | 'fail validation if '\ 59 | ":#{attribute_name} is unset; i.e., either the association "\ 60 | 'should have been defined with `required: '\ 61 | "#{required.inspect}`, or there " 62 | ) 63 | 64 | missing_option_string << 65 | if required 66 | 'should ' 67 | else 68 | 'should not ' 69 | end 70 | 71 | missing_option_string << "be a presence validation on :#{attribute_name}" 72 | end 73 | end 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/association_matchers/source_matcher.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | module AssociationMatchers 5 | # @private 6 | class SourceMatcher 7 | attr_accessor :missing_option 8 | 9 | def initialize(source, name) 10 | @source = source 11 | @name = name 12 | @missing_option = '' 13 | end 14 | 15 | def description 16 | "source => #{source}" 17 | end 18 | 19 | def matches?(subject) 20 | self.subject = ModelReflector.new(subject, name) 21 | 22 | if option_verifier.correct_for_string?(:source, source) 23 | true 24 | else 25 | self.missing_option = 26 | "#{name} should have #{source} as source option" 27 | false 28 | end 29 | end 30 | 31 | protected 32 | 33 | attr_accessor :subject, :source, :name 34 | 35 | def option_verifier 36 | @_option_verifier ||= OptionVerifier.new(subject) 37 | end 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/association_matchers/through_matcher.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | module AssociationMatchers 5 | # @private 6 | class ThroughMatcher 7 | attr_accessor :missing_option 8 | 9 | def initialize(through, name) 10 | @through = through 11 | @name = name 12 | @missing_option = '' 13 | end 14 | 15 | def description 16 | "through #{through}" 17 | end 18 | 19 | def matches?(subject) 20 | self.subject = ModelReflector.new(subject, name) 21 | through.nil? || association_set_properly? 22 | end 23 | 24 | def association_set_properly? 25 | through_association_exists? && through_association_correct? 26 | end 27 | 28 | def through_association_exists? 29 | if through_reflection.present? 30 | true 31 | else 32 | self.missing_option = 33 | "#{name} does not have any relationship to #{through}" 34 | false 35 | end 36 | end 37 | 38 | def through_reflection 39 | @_through_reflection ||= subject.reflect_on_association(through) 40 | end 41 | 42 | def through_association_correct? 43 | if option_verifier.correct_for_string?(:through, through) 44 | true 45 | else 46 | self.missing_option = 47 | "Expected #{name} to have #{name} through #{through}, "\ 48 | 'but got it through ' + 49 | option_verifier.actual_value_for(:through).to_s 50 | false 51 | end 52 | end 53 | 54 | protected 55 | 56 | attr_accessor :through, :name, :subject 57 | 58 | def option_verifier 59 | @_option_verifier ||= OptionVerifier.new(subject) 60 | end 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | # The `have_readonly_attribute` matcher tests usage of the 5 | # `attr_readonly` macro. 6 | # 7 | # class User < ActiveRecord::Base 8 | # attr_readonly :password 9 | # end 10 | # 11 | # # RSpec 12 | # RSpec.describe User, type: :model do 13 | # it { should have_readonly_attribute(:password) } 14 | # end 15 | # 16 | # # Minitest (Shoulda) 17 | # class UserTest < ActiveSupport::TestCase 18 | # should have_readonly_attribute(:password) 19 | # end 20 | # 21 | # @return [HaveReadonlyAttributeMatcher] 22 | # 23 | def have_readonly_attribute(value) 24 | HaveReadonlyAttributeMatcher.new(value) 25 | end 26 | 27 | # @private 28 | class HaveReadonlyAttributeMatcher 29 | def initialize(attribute) 30 | @attribute = attribute.to_s 31 | end 32 | 33 | attr_reader :failure_message, :failure_message_when_negated 34 | 35 | def matches?(subject) 36 | @subject = subject 37 | if readonly_attributes.include?(@attribute) 38 | @failure_message_when_negated = "Did not expect #{@attribute}"\ 39 | ' to be read-only' 40 | true 41 | else 42 | @failure_message = 43 | if readonly_attributes.empty? 44 | "#{class_name} attribute #{@attribute} " << 45 | 'is not read-only' 46 | else 47 | "#{class_name} is making " << 48 | "#{readonly_attributes.to_a.to_sentence} " << 49 | "read-only, but not #{@attribute}." 50 | end 51 | false 52 | end 53 | end 54 | 55 | def description 56 | "make #{@attribute} read-only" 57 | end 58 | 59 | private 60 | 61 | def readonly_attributes 62 | @_readonly_attributes ||= @subject.class.readonly_attributes || [] 63 | end 64 | 65 | def class_name 66 | @subject.class.name 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/uniqueness.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | # @private 5 | module Uniqueness 6 | end 7 | end 8 | end 9 | end 10 | 11 | require 'shoulda/matchers/active_record/uniqueness/model' 12 | require 'shoulda/matchers/active_record/uniqueness/namespace' 13 | require 'shoulda/matchers/active_record/uniqueness/test_model_creator' 14 | require 'shoulda/matchers/active_record/uniqueness/test_models' 15 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/uniqueness/model.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | # @private 5 | module Uniqueness 6 | # @private 7 | class Model 8 | def self.next_unique_copy_of(model_name, namespace) 9 | model = new(model_name, namespace) 10 | 11 | while model.already_exists? 12 | model = model.next 13 | end 14 | 15 | model 16 | end 17 | 18 | def initialize(name, namespace) 19 | @name = name 20 | @namespace = namespace 21 | end 22 | 23 | def already_exists? 24 | namespace.has?(name) 25 | end 26 | 27 | def next 28 | Model.new(name.next, namespace) 29 | end 30 | 31 | def symlink_to(parent) 32 | table_name = parent.table_name 33 | 34 | new_class = Class.new(parent) do 35 | define_singleton_method :table_name do 36 | table_name 37 | end 38 | 39 | define_singleton_method :base_class do 40 | self 41 | end 42 | end 43 | 44 | namespace.set(name, new_class) 45 | end 46 | 47 | def to_s 48 | [namespace, name].join('::') 49 | end 50 | 51 | protected 52 | 53 | attr_reader :name, :namespace 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/uniqueness/namespace.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | # @private 5 | module Uniqueness 6 | # @private 7 | class Namespace 8 | def initialize(constant) 9 | @constant = constant 10 | end 11 | 12 | def has?(name) 13 | constant.const_defined?(name) 14 | end 15 | 16 | def set(name, value) 17 | constant.const_set(name, value) 18 | end 19 | 20 | def clear 21 | constant.constants.each do |child_constant| 22 | constant.__send__(:remove_const, child_constant) 23 | end 24 | end 25 | 26 | def to_s 27 | constant.to_s 28 | end 29 | 30 | protected 31 | 32 | attr_reader :constant 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/uniqueness/test_model_creator.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | # @private 5 | module Uniqueness 6 | # @private 7 | class TestModelCreator 8 | def self.create(model_name, namespace) 9 | Mutex.new.synchronize do 10 | new(model_name, namespace).create 11 | end 12 | end 13 | 14 | def initialize(model_name, namespace) 15 | @model_name = model_name 16 | @namespace = namespace 17 | end 18 | 19 | def create 20 | new_model.tap do |new_model| 21 | new_model.symlink_to(existing_model) 22 | end 23 | end 24 | 25 | protected 26 | 27 | attr_reader :model_name, :namespace 28 | 29 | private 30 | 31 | def model_name_without_namespace 32 | model_name.demodulize 33 | end 34 | 35 | def new_model 36 | @_new_model ||= Model.next_unique_copy_of( 37 | model_name_without_namespace, 38 | namespace, 39 | ) 40 | end 41 | 42 | def existing_model 43 | @_existing_model ||= model_name.constantize 44 | end 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/active_record/uniqueness/test_models.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module ActiveRecord 4 | # @private 5 | module Uniqueness 6 | # @private 7 | module TestModels 8 | def self.create(model_name) 9 | TestModelCreator.create(model_name, root_namespace) 10 | end 11 | 12 | def self.remove_all 13 | root_namespace.clear 14 | end 15 | 16 | def self.root_namespace 17 | @_root_namespace ||= Namespace.new(self) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/configuration.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | # @private 4 | def self.configure 5 | yield configuration 6 | end 7 | 8 | # @private 9 | def self.integrations 10 | configuration.integrations 11 | end 12 | 13 | # @private 14 | def self.configuration 15 | @_configuration ||= Configuration.new 16 | end 17 | 18 | # @private 19 | class Configuration 20 | attr_reader :integrations 21 | 22 | def initialize 23 | @integrations = nil 24 | end 25 | 26 | def integrate(&block) 27 | @integrations = Integrations::Configuration.apply(&block) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/doublespeak.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Shoulda 4 | module Matchers 5 | # @private 6 | module Doublespeak 7 | class << self 8 | extend Forwardable 9 | 10 | def_delegators :world, :double_collection_for, 11 | :with_doubles_activated 12 | 13 | def world 14 | @_world ||= World.new 15 | end 16 | 17 | def debugging_enabled? 18 | ENV['DEBUG_DOUBLESPEAK'] == '1' 19 | end 20 | 21 | def debug(&block) 22 | if debugging_enabled? 23 | puts block.call # rubocop:disable Rails/Output 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | 31 | require 'shoulda/matchers/doublespeak/double' 32 | require 'shoulda/matchers/doublespeak/double_collection' 33 | require 'shoulda/matchers/doublespeak/double_implementation_registry' 34 | require 'shoulda/matchers/doublespeak/method_call' 35 | require 'shoulda/matchers/doublespeak/object_double' 36 | require 'shoulda/matchers/doublespeak/proxy_implementation' 37 | require 'shoulda/matchers/doublespeak/stub_implementation' 38 | require 'shoulda/matchers/doublespeak/world' 39 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/doublespeak/double_collection.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Doublespeak 4 | # @private 5 | class DoubleCollection 6 | def initialize(world, klass) 7 | @world = world 8 | @klass = klass 9 | @doubles_by_method_name = {} 10 | end 11 | 12 | def register_stub(method_name) 13 | register_double(method_name, :stub) 14 | end 15 | 16 | def register_proxy(method_name) 17 | register_double(method_name, :proxy) 18 | end 19 | 20 | def activate 21 | doubles_by_method_name.each_value(&:activate) 22 | end 23 | 24 | def deactivate 25 | doubles_by_method_name.each_value(&:deactivate) 26 | end 27 | 28 | def calls_by_method_name 29 | doubles_by_method_name.inject({}) do |hash, (method_name, double)| 30 | hash.merge! method_name => double.calls.map(&:args) 31 | end 32 | end 33 | 34 | def calls_to(method_name) 35 | double = doubles_by_method_name[method_name] 36 | 37 | if double 38 | double.calls 39 | else 40 | [] 41 | end 42 | end 43 | 44 | protected 45 | 46 | attr_reader :world, :klass, :doubles_by_method_name 47 | 48 | def register_double(method_name, implementation_type) 49 | doubles_by_method_name.fetch(method_name) do 50 | implementation = 51 | DoubleImplementationRegistry.find(implementation_type) 52 | double = Double.new(world, klass, method_name, implementation) 53 | doubles_by_method_name[method_name] = double 54 | double 55 | end 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/doublespeak/double_implementation_registry.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Doublespeak 4 | # @private 5 | module DoubleImplementationRegistry 6 | class << self 7 | def find(type) 8 | find_class!(type).create 9 | end 10 | 11 | def register(klass, type) 12 | registry[type] = klass 13 | end 14 | 15 | private 16 | 17 | def find_class!(type) 18 | registry.fetch(type) do 19 | raise ArgumentError, 'No double implementation class found for'\ 20 | " '#{type}'" 21 | end 22 | end 23 | 24 | def registry 25 | @_registry ||= {} 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/doublespeak/method_call.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Doublespeak 4 | # @private 5 | class MethodCall 6 | attr_accessor :return_value 7 | attr_reader :method_name, :args, :caller, :block, :object, :double 8 | 9 | def initialize(args) 10 | @method_name = args.fetch(:method_name) 11 | @args = args.fetch(:args) 12 | @caller = args.fetch(:caller) 13 | @block = args[:block] 14 | @double = args[:double] 15 | @object = args[:object] 16 | @return_value = nil 17 | end 18 | 19 | def with_return_value(return_value) 20 | dup.tap do |call| 21 | call.return_value = return_value 22 | end 23 | end 24 | 25 | def ==(other) 26 | other.is_a?(self.class) && 27 | method_name == other.method_name && 28 | args == other.args && 29 | block == other.block && 30 | double == other.double && 31 | object == other.object 32 | end 33 | 34 | def to_hash 35 | { method_name: method_name, args: args } 36 | end 37 | 38 | def inspect 39 | "#<#{self.class.name} #{to_hash.inspect}>" 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/doublespeak/object_double.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Doublespeak 4 | # @private 5 | class ObjectDouble < BasicObject 6 | attr_reader :calls 7 | 8 | def initialize 9 | @calls = [] 10 | @calls_by_method_name = {} 11 | end 12 | 13 | def calls_to(method_name) 14 | @calls_by_method_name[method_name] || [] 15 | end 16 | 17 | def respond_to?(_name, _include_private = nil) 18 | true 19 | end 20 | 21 | def respond_to_missing?(_name, _include_all) 22 | true 23 | end 24 | 25 | def method_missing(method_name, *args, &block) 26 | call = MethodCall.new( 27 | method_name: method_name, 28 | args: args, 29 | block: block, 30 | caller: ::Kernel.caller, 31 | ) 32 | calls << call 33 | (calls_by_method_name[method_name] ||= []) << call 34 | nil 35 | end 36 | 37 | protected 38 | 39 | attr_reader :calls_by_method_name 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/doublespeak/proxy_implementation.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Doublespeak 4 | # @private 5 | class ProxyImplementation 6 | extend Forwardable 7 | 8 | DoubleImplementationRegistry.register(self, :proxy) 9 | 10 | def_delegators :stub_implementation, :returns 11 | 12 | def self.create 13 | new(StubImplementation.new) 14 | end 15 | 16 | def initialize(stub_implementation) 17 | @stub_implementation = stub_implementation 18 | end 19 | 20 | def call(call) 21 | return_value = call.double.call_original_method(call) 22 | stub_implementation.call(call.with_return_value(return_value)) 23 | return_value 24 | end 25 | 26 | protected 27 | 28 | attr_reader :stub_implementation 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/doublespeak/stub_implementation.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Doublespeak 4 | # @private 5 | class StubImplementation 6 | DoubleImplementationRegistry.register(self, :stub) 7 | 8 | def self.create 9 | new 10 | end 11 | 12 | def initialize 13 | @implementation = proc { nil } 14 | end 15 | 16 | def returns(value = nil, &block) 17 | @implementation = block || proc { value } 18 | end 19 | 20 | def call(call) 21 | call.double.record_call(call) 22 | implementation.call(call) 23 | end 24 | 25 | protected 26 | 27 | attr_reader :implementation 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/doublespeak/world.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Doublespeak 4 | # @private 5 | class World 6 | def initialize 7 | @doubles_activated = false 8 | end 9 | 10 | def double_collection_for(klass) 11 | double_collections_by_class[klass] ||= 12 | DoubleCollection.new(self, klass) 13 | end 14 | 15 | def store_original_method_for(klass, method_name) 16 | original_methods_for_class(klass)[method_name] ||= 17 | klass.instance_method(method_name) 18 | end 19 | 20 | def original_method_for(klass, method_name) 21 | if original_methods_by_class.key?(klass) 22 | original_methods_by_class[klass][method_name] 23 | end 24 | end 25 | 26 | def with_doubles_activated 27 | @doubles_activated = true 28 | activate 29 | yield 30 | ensure 31 | @doubles_activated = false 32 | deactivate 33 | end 34 | 35 | def doubles_activated? 36 | @doubles_activated 37 | end 38 | 39 | private 40 | 41 | def activate 42 | double_collections_by_class.each_value(&:activate) 43 | end 44 | 45 | def deactivate 46 | double_collections_by_class.each_value(&:deactivate) 47 | end 48 | 49 | def double_collections_by_class 50 | @_double_collections_by_class ||= {} 51 | end 52 | 53 | def original_methods_by_class 54 | @_original_methods_by_class ||= {} 55 | end 56 | 57 | def original_methods_for_class(klass) 58 | original_methods_by_class[klass] ||= {} 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/error.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | # @private 4 | class Error < StandardError 5 | def self.create(attributes) 6 | allocate.tap do |error| 7 | attributes.each do |name, value| 8 | error.__send__("#{name}=", value) 9 | end 10 | 11 | error.__send__(:initialize) 12 | end 13 | end 14 | 15 | def initialize(*args) 16 | super 17 | @message = message 18 | end 19 | 20 | def message 21 | '' 22 | end 23 | 24 | def inspect 25 | %(#<#{self.class}: #{message}>) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/independent.rb: -------------------------------------------------------------------------------- 1 | require 'shoulda/matchers/independent/delegate_method_matcher' 2 | require 'shoulda/matchers/independent/delegate_method_matcher/target_not_defined_error' 3 | 4 | module Shoulda 5 | module Matchers 6 | # This module provides matchers that are used to test behavior outside of 7 | # Rails-specific classes. 8 | module Independent 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/independent/delegate_method_matcher/target_not_defined_error.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Independent 4 | class DelegateMethodMatcher 5 | # @private 6 | class DelegateObjectNotSpecified < StandardError 7 | def message 8 | 'Delegation needs a target. Use the #to method to define one, e.g. 9 | `post_office.should delegate(:deliver_mail).to(:mailman)`'.squish 10 | end 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | # @private 4 | module Integrations 5 | class << self 6 | def register_library(klass, name) 7 | library_registry.register(klass, name) 8 | end 9 | 10 | def find_library!(name) 11 | library_registry.find!(name) 12 | end 13 | 14 | def register_test_framework(klass, name) 15 | test_framework_registry.register(klass, name) 16 | end 17 | 18 | def find_test_framework!(name) 19 | test_framework_registry.find!(name) 20 | end 21 | 22 | private 23 | 24 | def library_registry 25 | @_library_registry ||= Registry.new 26 | end 27 | 28 | def test_framework_registry 29 | @_test_framework_registry ||= Registry.new 30 | end 31 | end 32 | end 33 | end 34 | end 35 | 36 | require 'shoulda/matchers/integrations/configuration' 37 | require 'shoulda/matchers/integrations/configuration_error' 38 | require 'shoulda/matchers/integrations/inclusion' 39 | require 'shoulda/matchers/integrations/rails' 40 | require 'shoulda/matchers/integrations/registry' 41 | 42 | require 'shoulda/matchers/integrations/libraries' 43 | require 'shoulda/matchers/integrations/test_frameworks' 44 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/configuration.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module Shoulda 4 | module Matchers 5 | module Integrations 6 | # @private 7 | class Configuration 8 | def self.apply(&block) 9 | new(&block).apply 10 | end 11 | 12 | attr_reader :test_frameworks 13 | 14 | def initialize(&block) 15 | @test_frameworks = Set.new 16 | @libraries = Set.new 17 | 18 | test_framework :missing_test_framework 19 | library :missing_library 20 | 21 | block.call(self) 22 | end 23 | 24 | def test_framework(name) 25 | clear_default_test_framework 26 | @test_frameworks << Integrations.find_test_framework!(name) 27 | end 28 | 29 | def library(name) 30 | @libraries << Integrations.find_library!(name) 31 | end 32 | 33 | def apply 34 | if no_test_frameworks_added? && no_libraries_added? 35 | raise ConfigurationError, <<EOT 36 | shoulda-matchers is not configured correctly. You need to specify at least one 37 | test framework and/or library. For example: 38 | 39 | Shoulda::Matchers.configure do |config| 40 | config.integrate do |with| 41 | with.test_framework :rspec 42 | with.library :rails 43 | end 44 | end 45 | EOT 46 | end 47 | 48 | @test_frameworks.each do |test_framework| 49 | test_framework.include(Shoulda::Matchers::Independent) 50 | @libraries.each { |library| library.integrate_with(test_framework) } 51 | end 52 | 53 | self 54 | end 55 | 56 | private 57 | 58 | def clear_default_test_framework 59 | @test_frameworks.select!(&:present?) 60 | end 61 | 62 | def no_test_frameworks_added? 63 | @test_frameworks.empty? || @test_frameworks.none?(&:present?) 64 | end 65 | 66 | def no_libraries_added? 67 | @libraries.empty? 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/configuration_error.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | # @private 5 | class ConfigurationError < StandardError 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/inclusion.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | # @private 5 | module Inclusion 6 | def include_into(mod, *other_mods, &block) 7 | mods_to_include = other_mods.dup 8 | mods_to_extend = other_mods.dup 9 | 10 | if block 11 | mods_to_include << Module.new(&block) 12 | end 13 | 14 | mod.__send__(:include, *mods_to_include) 15 | mod.extend(*mods_to_extend) 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/libraries.rb: -------------------------------------------------------------------------------- 1 | require 'shoulda/matchers/integrations/libraries/action_controller' 2 | require 'shoulda/matchers/integrations/libraries/active_model' 3 | require 'shoulda/matchers/integrations/libraries/active_record' 4 | require 'shoulda/matchers/integrations/libraries/missing_library' 5 | require 'shoulda/matchers/integrations/libraries/rails' 6 | require 'shoulda/matchers/integrations/libraries/routing' 7 | 8 | module Shoulda 9 | module Matchers 10 | module Integrations 11 | # @private 12 | module Libraries 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/libraries/action_controller.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | module Libraries 5 | # @private 6 | class ActionController 7 | Integrations.register_library(self, :action_controller) 8 | 9 | include Integrations::Inclusion 10 | include Integrations::Rails 11 | 12 | def integrate_with(test_framework) 13 | test_framework.include(matchers_module, type: :controller) 14 | 15 | tap do |instance| 16 | ActiveSupport.on_load(:action_controller_test_case, run_once: true) do 17 | instance.include_into(::ActionController::TestCase, instance.matchers_module) do 18 | def subject # rubocop:disable Lint/NestedMethodDefinition 19 | @controller 20 | end 21 | end 22 | end 23 | end 24 | end 25 | 26 | def matchers_module 27 | Shoulda::Matchers::ActionController 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/libraries/active_model.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | module Libraries 5 | # @private 6 | class ActiveModel 7 | Integrations.register_library(self, :active_model) 8 | 9 | include Integrations::Inclusion 10 | include Integrations::Rails 11 | 12 | def integrate_with(test_framework) 13 | test_framework.include(matchers_module, type: :model) 14 | include_into(ActiveSupport::TestCase, matchers_module) 15 | end 16 | 17 | private 18 | 19 | def matchers_module 20 | Shoulda::Matchers::ActiveModel 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/libraries/active_record.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | module Libraries 5 | # @private 6 | class ActiveRecord 7 | Integrations.register_library(self, :active_record) 8 | 9 | include Integrations::Inclusion 10 | include Integrations::Rails 11 | 12 | def integrate_with(test_framework) 13 | test_framework.include(matchers_module, type: :model) 14 | include_into(ActiveSupport::TestCase, matchers_module) 15 | end 16 | 17 | private 18 | 19 | def matchers_module 20 | Shoulda::Matchers::ActiveRecord 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/libraries/missing_library.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | module Libraries 5 | # @private 6 | class MissingLibrary 7 | Integrations.register_library(self, :missing_library) 8 | 9 | def integrate_with(test_framework) 10 | end 11 | 12 | def rails? 13 | false 14 | end 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/libraries/rails.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | module Libraries 5 | # @private 6 | class Rails 7 | Integrations.register_library(self, :rails) 8 | 9 | include Integrations::Rails 10 | 11 | SUB_LIBRARIES = [ 12 | :active_model, 13 | :active_record, 14 | :action_controller, 15 | :routing, 16 | ].freeze 17 | 18 | def integrate_with(test_framework) 19 | Shoulda::Matchers.assertion_exception_class = 20 | ActiveSupport::TestCase::Assertion 21 | 22 | SUB_LIBRARIES.each do |name| 23 | library = Integrations.find_library!(name) 24 | library.integrate_with(test_framework) 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/libraries/routing.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | module Libraries 5 | # @private 6 | class Routing 7 | Integrations.register_library(self, :routing) 8 | 9 | include Integrations::Inclusion 10 | include Integrations::Rails 11 | 12 | def integrate_with(test_framework) 13 | test_framework.include(matchers_module, type: :routing) 14 | 15 | tap do |instance| 16 | ActiveSupport.on_load(:action_controller_test_case, run_once: true) do 17 | instance.include_into(::ActionController::TestCase, instance.matchers_module) 18 | end 19 | end 20 | end 21 | 22 | def matchers_module 23 | Shoulda::Matchers::Routing 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/rails.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | # @private 5 | module Rails 6 | def rails? 7 | true 8 | end 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/registry.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | # @private 5 | class Registry 6 | def register(klass, name) 7 | registry[name] = klass 8 | end 9 | 10 | def find!(name) 11 | find_class!(name).new 12 | end 13 | 14 | private 15 | 16 | def registry 17 | @_registry ||= {} 18 | end 19 | 20 | def find_class!(name) 21 | registry.fetch(name) do 22 | raise ArgumentError, "'#{name}' is not registered" 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/test_frameworks.rb: -------------------------------------------------------------------------------- 1 | require 'shoulda/matchers/integrations/test_frameworks/active_support_test_case' 2 | require 'shoulda/matchers/integrations/test_frameworks/minitest_4' 3 | require 'shoulda/matchers/integrations/test_frameworks/minitest_5' 4 | require 'shoulda/matchers/integrations/test_frameworks/missing_test_framework' 5 | require 'shoulda/matchers/integrations/test_frameworks/rspec' 6 | require 'shoulda/matchers/integrations/test_frameworks/test_unit' 7 | 8 | module Shoulda 9 | module Matchers 10 | module Integrations 11 | # @private 12 | module TestFrameworks 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/test_frameworks/active_support_test_case.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | module TestFrameworks 5 | # @private 6 | class ActiveSupportTestCase 7 | Integrations.register_test_framework(self, :active_support_test_case) 8 | 9 | def validate! 10 | end 11 | 12 | def include(*modules, **_options) 13 | test_case_class.include(*modules) 14 | end 15 | 16 | def n_unit? 17 | true 18 | end 19 | 20 | def present? 21 | true 22 | end 23 | 24 | protected 25 | 26 | attr_reader :configuration 27 | 28 | private 29 | 30 | def test_case_class 31 | ActiveSupport::TestCase 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/test_frameworks/minitest_4.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | module TestFrameworks 5 | # @private 6 | class Minitest4 7 | Integrations.register_test_framework(self, :minitest_4) 8 | 9 | def validate! 10 | end 11 | 12 | def include(*modules, **_options) 13 | test_case_class.class_eval do 14 | include(*modules) 15 | extend(*modules) 16 | end 17 | end 18 | 19 | def n_unit? 20 | true 21 | end 22 | 23 | def present? 24 | true 25 | end 26 | 27 | private 28 | 29 | def test_case_class 30 | MiniTest::Unit::TestCase 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/test_frameworks/minitest_5.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | module TestFrameworks 5 | # @private 6 | class Minitest5 7 | Integrations.register_test_framework(self, :minitest_5) 8 | Integrations.register_test_framework(self, :minitest) 9 | 10 | def validate! 11 | end 12 | 13 | def include(*modules, **_options) 14 | test_case_class.class_eval do 15 | include(*modules) 16 | extend(*modules) 17 | end 18 | end 19 | 20 | def n_unit? 21 | true 22 | end 23 | 24 | def present? 25 | true 26 | end 27 | 28 | private 29 | 30 | def test_case_class 31 | Minitest::Test 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/test_frameworks/missing_test_framework.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | module TestFrameworks 5 | # @private 6 | class MissingTestFramework 7 | Integrations.register_test_framework(self, :missing_test_framework) 8 | 9 | def validate! 10 | raise TestFrameworkNotConfigured, <<-EOT 11 | You need to set a test framework. Please add the following to your 12 | test helper: 13 | 14 | Shoulda::Matchers.configure do |config| 15 | config.integrate do |with| 16 | # Choose one: 17 | with.test_framework :rspec 18 | with.test_framework :minitest # or, :minitest_5 19 | with.test_framework :minitest_4 20 | with.test_framework :test_unit 21 | end 22 | end 23 | EOT 24 | end 25 | 26 | def include(*modules, **options) 27 | end 28 | 29 | def n_unit? 30 | false 31 | end 32 | 33 | def present? 34 | false 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/test_frameworks/rspec.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | module TestFrameworks 5 | # @private 6 | class Rspec 7 | Integrations.register_test_framework(self, :rspec) 8 | 9 | def validate! 10 | end 11 | 12 | def include(*modules, **options) 13 | ::RSpec.configure do |config| 14 | config.include(*modules, **options) 15 | end 16 | end 17 | 18 | def n_unit? 19 | false 20 | end 21 | 22 | def present? 23 | true 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/integrations/test_frameworks/test_unit.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | module Integrations 4 | module TestFrameworks 5 | # @private 6 | class TestUnit 7 | Integrations.register_test_framework(self, :test_unit) 8 | 9 | def validate! 10 | end 11 | 12 | def include(*modules, **_options) 13 | test_case_class.class_eval do 14 | include(*modules) 15 | extend(*modules) 16 | end 17 | end 18 | 19 | def n_unit? 20 | true 21 | end 22 | 23 | def present? 24 | true 25 | end 26 | 27 | private 28 | 29 | def test_case_class 30 | ::Test::Unit::TestCase 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/matcher_context.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | # @private 4 | class MatcherContext 5 | def initialize(context) 6 | @context = context 7 | end 8 | 9 | def subject_is_a_class? 10 | if inside_a_shoulda_context_project? && outside_a_should_block? 11 | assume_that_subject_is_not_a_class 12 | else 13 | context.subject.is_a?(Class) 14 | end 15 | end 16 | 17 | protected 18 | 19 | attr_reader :context 20 | 21 | private 22 | 23 | def inside_a_shoulda_context_project? 24 | defined?(Shoulda::Context) 25 | end 26 | 27 | def outside_a_should_block? 28 | context.is_a?(Class) 29 | end 30 | 31 | def assume_that_subject_is_not_a_class 32 | false 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/routing.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | # @private 4 | module Routing 5 | def route(method, path, port: nil) 6 | ActionController::RouteMatcher.new(self, method, path, port: port) 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/version.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | # @private 4 | VERSION = '6.5.0'.freeze 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/shoulda/matchers/warn.rb: -------------------------------------------------------------------------------- 1 | module Shoulda 2 | module Matchers 3 | # @private 4 | TERMINAL_MAX_WIDTH = 72 5 | 6 | # @private 7 | def self.warn(message) 8 | header = 'Warning from shoulda-matchers:' 9 | divider = '*' * TERMINAL_MAX_WIDTH 10 | wrapped_message = word_wrap(message) 11 | full_message = [ 12 | divider, 13 | [header, wrapped_message.strip].join("\n\n"), 14 | divider, 15 | ].join("\n") 16 | 17 | Kernel.warn(full_message) 18 | end 19 | 20 | # @private 21 | def self.warn_about_deprecated_method(old_method, new_method) 22 | warn <<EOT 23 | #{old_method} is deprecated and will be removed in the next major 24 | release. Please use #{new_method} instead. 25 | EOT 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /script/install_gems_in_all_appraisals: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | SUPPORTED_VERSIONS=$(script/supported_ruby_versions) 6 | 7 | install-gems-for-version() { 8 | local version="$1" 9 | (export RBENV_VERSION=$version; bundle && bundle exec appraisal install) 10 | } 11 | 12 | for version in $SUPPORTED_VERSIONS; do 13 | echo 14 | echo "*** Installing gems for $version ***" 15 | install-gems-for-version $version 16 | done 17 | -------------------------------------------------------------------------------- /script/run_all_tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | SUPPORTED_VERSIONS=$(script/supported_ruby_versions) 6 | 7 | run-tests-for-version() { 8 | local version="$1" 9 | (export RBENV_VERSION=$version; bundle exec rake) 10 | } 11 | 12 | for version in $SUPPORTED_VERSIONS; do 13 | echo 14 | echo "*** Running tests for $version ***" 15 | run-tests-for-version $version 16 | done 17 | -------------------------------------------------------------------------------- /script/supported_ruby_versions: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'yaml' 4 | 5 | travis_config_path = File.expand_path('../.travis.yml', __dir__) 6 | travis_config = YAML.load_file(travis_config_path) 7 | puts travis_config.fetch('rvm').join(' ') 8 | -------------------------------------------------------------------------------- /script/update_gem_in_all_appraisals: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | SUPPORTED_VERSIONS=$(script/supported_ruby_versions) 6 | gem="$1" 7 | 8 | update-gem-for-version() { 9 | local version="$1" 10 | (export RBENV_VERSION=$version; bundle update "$gem"; bundle exec appraisal update "$gem") 11 | } 12 | 13 | for version in $SUPPORTED_VERSIONS; do 14 | echo 15 | echo "*** Updating $gem for $version ***" 16 | update-gem-for-version $version 17 | done 18 | -------------------------------------------------------------------------------- /script/update_gems_in_all_appraisals: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | SUPPORTED_VERSIONS=$(script/supported_ruby_versions) 6 | 7 | update-gems-for-version() { 8 | local version="$1" 9 | (export RBENV_VERSION=$version; bundle update "${@:2}"; bundle exec appraisal update "${@:2}") 10 | } 11 | 12 | for version in $SUPPORTED_VERSIONS; do 13 | echo 14 | echo "*** Updating gems for $version ***" 15 | update-gems-for-version "$version" "$@" 16 | done 17 | -------------------------------------------------------------------------------- /shoulda-matchers.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.join(File.dirname(__FILE__), 'lib') 2 | require 'shoulda/matchers/version' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'shoulda-matchers' 6 | s.version = Shoulda::Matchers::VERSION.dup 7 | s.authors = [ 8 | 'Tammer Saleh', 9 | 'Joe Ferris', 10 | 'Ryan McGeary', 11 | 'Dan Croak', 12 | 'Matt Jankowski', 13 | 'Stafford Brunk', 14 | 'Elliot Winkler', 15 | ] 16 | s.date = Time.now.strftime('%Y-%m-%d') 17 | s.email = 'support@thoughtbot.com' 18 | s.homepage = 'https://matchers.shoulda.io/' 19 | s.summary = 'Simple one-liner tests for common Rails functionality' 20 | s.license = 'MIT' 21 | s.description = <<~DESC.tr("\n", ' ').squeeze(' ') 22 | Shoulda Matchers provides RSpec- and Minitest-compatible one-liners to test 23 | common Rails functionality that, if written by hand, would be much 24 | longer, more complex, and error-prone. 25 | DESC 26 | 27 | s.metadata = { 28 | 'bug_tracker_uri' => 'https://github.com/thoughtbot/shoulda-matchers/issues', 29 | 'changelog_uri' => 'https://github.com/thoughtbot/shoulda-matchers/blob/main/CHANGELOG.md', 30 | 'documentation_uri' => 'https://matchers.shoulda.io/docs', 31 | 'homepage_uri' => 'https://matchers.shoulda.io', 32 | 'source_code_uri' => 'https://github.com/thoughtbot/shoulda-matchers', 33 | } 34 | 35 | s.files = Dir['{docs,lib}/**/*', 'README.md', 'LICENSE', 36 | 'shoulda-matchers.gemspec'] 37 | s.require_paths = ['lib'] 38 | 39 | s.required_ruby_version = '>= 3.0.5' 40 | s.add_dependency('activesupport', '>= 5.2.0') 41 | end 42 | -------------------------------------------------------------------------------- /spec/acceptance/active_model_integration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'acceptance_spec_helper' 2 | 3 | describe 'shoulda-matchers integrates with an ActiveModel project' do 4 | before do 5 | create_active_model_project 6 | 7 | write_file 'lib/user.rb', <<-FILE 8 | require 'active_model' 9 | 10 | class User 11 | include ActiveModel::Validations 12 | attr_accessor :gender 13 | 14 | validates :gender, inclusion: { in: %w(male female) } 15 | end 16 | FILE 17 | 18 | write_file 'spec/user_spec.rb', <<-FILE 19 | require 'spec_helper' 20 | require 'user' 21 | include Shoulda::Matchers::ActiveModel 22 | 23 | describe User do 24 | context 'when gender is valid' do 25 | it { is_expected.to validate_inclusion_of(:gender).in_array(%w(male female)) } 26 | end 27 | context 'when gender is invalid' do 28 | it { is_expected.to validate_inclusion_of(:gender).in_array(%w(transgender female)) } 29 | end 30 | end 31 | FILE 32 | 33 | write_file 'load_dependencies.rb', <<-FILE 34 | require 'active_model' 35 | require 'shoulda-matchers' 36 | 37 | puts ActiveModel::VERSION::STRING 38 | puts "Loaded all dependencies without errors" 39 | FILE 40 | 41 | updating_bundle do 42 | add_rspec_to_project 43 | add_shoulda_matchers_to_project( 44 | manually: true, 45 | with_configuration: false, 46 | ) 47 | 48 | write_file 'spec/spec_helper.rb', <<-FILE 49 | require 'active_model' 50 | require 'shoulda-matchers' 51 | 52 | Shoulda::Matchers.configure do |config| 53 | config.integrate do |with| 54 | with.test_framework :rspec 55 | 56 | with.library :active_model 57 | end 58 | end 59 | FILE 60 | end 61 | end 62 | 63 | context 'when using active model library' do 64 | it 'and loads without errors' do 65 | result = run_command_within_bundle('ruby load_dependencies.rb') 66 | 67 | expect(result).to have_output('Loaded all dependencies without errors') 68 | end 69 | 70 | it 'allows use of inclusion matcher from active model library' do 71 | result = run_rspec_tests('spec/user_spec.rb') 72 | 73 | expect(result).to have_output('2 examples, 1 failure') 74 | expect(result).to have_output( 75 | 'gender: ["is not included in the list"]', 76 | ) 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/acceptance/multiple_libraries_integration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'acceptance_spec_helper' 2 | 3 | describe 'shoulda-matchers integrates with multiple libraries' do 4 | before do 5 | create_rails_application 6 | 7 | write_file 'db/migrate/1_create_users.rb', <<-FILE 8 | class CreateUsers < #{migration_class_name} 9 | def self.up 10 | create_table :users do |t| 11 | t.string :name 12 | end 13 | end 14 | end 15 | FILE 16 | 17 | run_rake_tasks!('db:drop', 'db:create', 'db:migrate') 18 | 19 | write_file 'app/models/user.rb', <<-FILE 20 | class User < ActiveRecord::Base 21 | validates_presence_of :name 22 | validates_uniqueness_of :name 23 | end 24 | FILE 25 | 26 | add_rspec_file 'spec/models/user_spec.rb', <<-FILE 27 | describe User do 28 | subject { User.new(name: "John Smith") } 29 | it { should validate_presence_of(:name) } 30 | it { should validate_uniqueness_of(:name) } 31 | end 32 | FILE 33 | 34 | updating_bundle do 35 | add_rspec_rails_to_project! 36 | add_shoulda_matchers_to_project( 37 | test_frameworks: [:rspec], 38 | libraries: [:active_record, :active_model], 39 | ) 40 | end 41 | end 42 | 43 | context 'when using both active_record and active_model libraries' do 44 | it 'allows the use of matchers from both libraries' do 45 | result = run_rspec_suite 46 | expect(result).to have_output('2 examples, 0 failures') 47 | expect(result).to have_output( 48 | 'is expected to validate that :name cannot be empty/falsy', 49 | ) 50 | expect(result).to have_output( 51 | 'is expected to validate that :name is case-sensitively unique', 52 | ) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/acceptance_spec_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative 'support/tests/current_bundle' 2 | 3 | Tests::CurrentBundle.instance.assert_appraisal! 4 | 5 | #--- 6 | 7 | require 'rspec/core' 8 | 9 | require 'spec_helper' 10 | 11 | Dir[File.join(File.expand_path('support/acceptance/**/*.rb', __dir__))].sort.each do |file| 12 | require file 13 | end 14 | 15 | RSpec.configure do |config| 16 | if config.respond_to?(:infer_spec_type_from_file_location!) 17 | config.infer_spec_type_from_file_location! 18 | end 19 | 20 | AcceptanceTests::Helpers.configure_example_group(config) 21 | 22 | config.include AcceptanceTests::Matchers 23 | end 24 | -------------------------------------------------------------------------------- /spec/doublespeak_spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'shoulda/matchers/doublespeak' 2 | require 'spec_helper' 3 | -------------------------------------------------------------------------------- /spec/report_warnings.rb: -------------------------------------------------------------------------------- 1 | require 'warnings_logger' 2 | 3 | WarningsLogger.configure do |config| 4 | config.project_name = 'shoulda-matchers' 5 | config.project_directory = Pathname.new('..').expand_path(__dir__) 6 | end 7 | 8 | WarningsLogger.enable 9 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | PROJECT_ROOT = File.expand_path('..', __dir__) 2 | $LOAD_PATH << File.join(PROJECT_ROOT, 'lib') 3 | 4 | require 'pry' 5 | require 'pry-byebug' if RUBY_VERSION < '3.2' 6 | 7 | require 'rspec' 8 | 9 | RSpec.configure do |config| 10 | config.expect_with :rspec do |c| 11 | c.syntax = :expect 12 | end 13 | 14 | config.order = :random 15 | config.default_formatter = 'doc' 16 | config.mock_with :rspec 17 | config.example_status_persistence_file_path = 'spec/examples.txt' 18 | end 19 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helpers/active_model_helpers' 2 | require_relative 'helpers/active_record_helpers' 3 | require_relative 'helpers/base_helpers' 4 | require_relative 'helpers/command_helpers' 5 | require_relative 'helpers/gem_helpers' 6 | require_relative 'helpers/n_unit_helpers' 7 | require_relative 'helpers/rails_migration_helpers' 8 | require_relative 'helpers/rails_version_helpers' 9 | require_relative 'helpers/rspec_helpers' 10 | require_relative 'helpers/ruby_version_helpers' 11 | require_relative 'helpers/step_helpers' 12 | 13 | module AcceptanceTests 14 | module Helpers 15 | def self.configure_example_group(example_group) 16 | example_group.include(self) 17 | 18 | example_group.before do 19 | fs.clean 20 | end 21 | end 22 | 23 | include ActiveModelHelpers 24 | include ActiveRecordHelpers 25 | include BaseHelpers 26 | include CommandHelpers 27 | include GemHelpers 28 | include NUnitHelpers 29 | include RailsMigrationHelpers 30 | include RailsVersionHelpers 31 | include RspecHelpers 32 | include RubyVersionHelpers 33 | include StepHelpers 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers/active_model_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'gem_helpers' 2 | 3 | module AcceptanceTests 4 | module ActiveModelHelpers 5 | include GemHelpers 6 | 7 | def active_model_version 8 | bundle_version_of('activemodel') 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers/active_record_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'gem_helpers' 2 | 3 | module AcceptanceTests 4 | module ActiveRecordHelpers 5 | include GemHelpers 6 | 7 | def active_record_version 8 | bundle_version_of('activerecord') 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers/array_helpers.rb: -------------------------------------------------------------------------------- 1 | module AcceptanceTests 2 | module ArrayHelpers 3 | def to_sentence(array) 4 | case array.size 5 | when 1 6 | array[0] 7 | when 2 8 | array.join(' and ') 9 | else 10 | to_sentence(array[1..-2].join(', '), [array[-1]]) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers/base_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../tests/bundle' 2 | require_relative '../../tests/database' 3 | require_relative '../../tests/filesystem' 4 | 5 | module AcceptanceTests 6 | module BaseHelpers 7 | def fs 8 | @_fs ||= Tests::Filesystem.new 9 | end 10 | 11 | def bundle 12 | @_bundle ||= Tests::Bundle.new 13 | end 14 | 15 | def database 16 | @_database ||= Tests::Database.instance 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers/command_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base_helpers' 2 | require_relative '../../tests/command_runner' 3 | 4 | module AcceptanceTests 5 | module CommandHelpers 6 | include BaseHelpers 7 | extend RSpec::Matchers::DSL 8 | 9 | def run_command(*args) 10 | Tests::CommandRunner.run(*args) do |runner| 11 | runner.directory = fs.project_directory 12 | yield runner if block_given? 13 | end 14 | end 15 | 16 | def run_command!(*args) 17 | run_command(*args) do |runner| 18 | runner.run_successfully = true 19 | yield runner if block_given? 20 | end 21 | end 22 | 23 | def run_command_isolated_from_bundle(*args) 24 | run_command(*args) do |runner| 25 | runner.around_command do |run_command| 26 | Bundler.with_original_env(&run_command) 27 | end 28 | 29 | yield runner if block_given? 30 | end 31 | end 32 | 33 | def run_command_isolated_from_bundle!(*args) 34 | run_command_isolated_from_bundle(*args) do |runner| 35 | runner.run_successfully = true 36 | yield runner if block_given? 37 | end 38 | end 39 | 40 | def run_command_within_bundle(*args) 41 | run_command_isolated_from_bundle(*args) do |runner| 42 | runner.command_prefix = "bundle _#{bundle.version}_ exec" 43 | runner.env['BUNDLE_GEMFILE'] = fs.find_in_project('Gemfile').to_s 44 | 45 | yield runner if block_given? 46 | end 47 | end 48 | 49 | def run_command_within_bundle!(*args) 50 | run_command_within_bundle(*args) do |runner| 51 | runner.run_successfully = true 52 | yield runner if block_given? 53 | end 54 | end 55 | 56 | def run_rake_tasks(*tasks) 57 | options = tasks.last.is_a?(Hash) ? tasks.pop : {} 58 | args = ['rake', *tasks, '--trace'] + [options] 59 | run_command_within_bundle(*args) 60 | end 61 | 62 | def run_rake_tasks!(*tasks) 63 | options = tasks.last.is_a?(Hash) ? tasks.pop : {} 64 | args = ['rake', *tasks, '--trace'] + [options] 65 | run_command_within_bundle!(*args) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers/file_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base_helpers' 2 | 3 | module AcceptanceTests 4 | module FileHelpers 5 | include BaseHelpers 6 | 7 | def append_to_file(path, content, options = {}) 8 | fs.append_to_file(path, content, options) 9 | end 10 | 11 | def remove_from_file(path, pattern) 12 | fs.remove_from_file(path, pattern) 13 | end 14 | 15 | def write_file(path, content) 16 | fs.write(path, content) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers/gem_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base_helpers' 2 | require_relative 'command_helpers' 3 | require_relative 'file_helpers' 4 | 5 | module AcceptanceTests 6 | module GemHelpers 7 | include BaseHelpers 8 | include CommandHelpers 9 | include FileHelpers 10 | 11 | def add_gem(gem, *args) 12 | bundle.add_gem(gem, *args) 13 | end 14 | 15 | def install_gems 16 | bundle.install_gems 17 | end 18 | 19 | def updating_bundle(&block) 20 | bundle.updating(&block) 21 | end 22 | 23 | def bundle_version_of(gem) 24 | bundle.version_of(gem) 25 | end 26 | 27 | def bundle_includes?(gem) 28 | bundle.includes?(gem) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers/minitest_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'gem_helpers' 2 | 3 | module AcceptanceTests 4 | module MinitestHelpers 5 | include GemHelpers 6 | 7 | def minitest_version 8 | bundle_version_of('minitest') 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers/n_unit_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'rails_version_helpers' 2 | 3 | module AcceptanceTests 4 | module NUnitHelpers 5 | include RailsVersionHelpers 6 | 7 | def n_unit_test_case_superclass 8 | case default_test_framework 9 | when :test_unit then 'Test::Unit::TestCase' 10 | when :minitest_4 then 'MiniTest::Unit::TestCase' 11 | else 'Minitest::Test' 12 | end 13 | end 14 | 15 | def default_test_framework 16 | :minitest 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers/pluralization_helpers.rb: -------------------------------------------------------------------------------- 1 | module AcceptanceTests 2 | module PluralizationHelpers 3 | def pluralize(count, singular_version, plural_version = nil) 4 | plural_version ||= "#{singular_version}s" 5 | 6 | if count == 1 7 | "#{count} #{singular_version}" 8 | else 9 | "#{count} #{plural_version}" 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers/rails_migration_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'gem_helpers' 2 | 3 | module AcceptanceTests 4 | module RailsMigrationHelpers 5 | include RailsVersionHelpers 6 | 7 | def migration_class_name 8 | "ActiveRecord::Migration[#{rails_version_for_migration}]" 9 | end 10 | 11 | private 12 | 13 | def rails_version_for_migration 14 | rails_version.to_s.split('.')[0..1].join('.') 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers/rails_version_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'gem_helpers' 2 | 3 | module AcceptanceTests 4 | module RailsVersionHelpers 5 | include GemHelpers 6 | 7 | def rails_version 8 | bundle_version_of('rails') 9 | end 10 | 11 | def rails_6_x? 12 | rails_version =~ '~> 6.0' 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers/rspec_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative 'gem_helpers' 2 | 3 | module AcceptanceTests 4 | module RspecHelpers 5 | include GemHelpers 6 | 7 | def rspec_core_version 8 | bundle_version_of('rspec-core') 9 | end 10 | 11 | def rspec_expectations_version 12 | bundle_version_of('rspec-expectations') 13 | end 14 | 15 | def rspec_rails_version 16 | bundle_version_of('rspec-rails') 17 | end 18 | 19 | def add_rspec_file(path, content) 20 | content = "require 'rails_helper'\n#{content}" 21 | write_file path, content 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/support/acceptance/helpers/ruby_version_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../tests/version' 2 | 3 | module AcceptanceTests 4 | module RubyVersionHelpers 5 | def ruby_version 6 | Tests::Version.new(RUBY_VERSION) 7 | end 8 | 9 | def ruby_gt_3_1? 10 | ruby_version >= '3.1' 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/acceptance/matchers/have_output.rb: -------------------------------------------------------------------------------- 1 | module AcceptanceTests 2 | module Matchers 3 | def have_output(output) 4 | HaveOutputMatcher.new(output) 5 | end 6 | 7 | class HaveOutputMatcher 8 | def initialize(output) 9 | @output = output 10 | end 11 | 12 | def matches?(runner) 13 | @runner = runner 14 | runner.has_output?(output) 15 | end 16 | 17 | def failure_message 18 | "Expected command to have output, but did not.\n\n"\ 19 | "Command: #{runner.formatted_command}\n\n"\ 20 | "Expected output:\n" + 21 | output.inspect + "\n\n"\ 22 | "Actual output:\n" + 23 | runner.output 24 | end 25 | 26 | protected 27 | 28 | attr_reader :output, :runner 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/support/acceptance/matchers/indicate_number_of_tests_was_run_matcher.rb: -------------------------------------------------------------------------------- 1 | require_relative '../helpers/pluralization_helpers' 2 | require_relative '../helpers/rails_version_helpers' 3 | 4 | module AcceptanceTests 5 | module Matchers 6 | def indicate_number_of_tests_was_run(expected_output) 7 | IndicateNumberOfTestsWasRunMatcher.new(expected_output) 8 | end 9 | 10 | class IndicateNumberOfTestsWasRunMatcher 11 | include PluralizationHelpers 12 | include RailsVersionHelpers 13 | 14 | def initialize(number) 15 | @number = number 16 | end 17 | 18 | def matches?(runner) 19 | @runner = runner 20 | expected_output === actual_output 21 | end 22 | 23 | def failure_message 24 | message = "Expected output to indicate that #{some_tests_were_run}.\n" + 25 | "Expected output: #{expected_output}\n" 26 | 27 | message << 28 | if actual_output.empty? 29 | 'Actual output: (empty)' 30 | else 31 | "Actual output:\n#{actual_output}" 32 | end 33 | 34 | message 35 | end 36 | 37 | protected 38 | 39 | attr_reader :number, :runner 40 | 41 | private 42 | 43 | def expected_output 44 | /#{number} (?:tests?|runs?|examples?)(?:, #{number} assertions)?, 0 failures(?:, 0 errors(?:, 0 skips)?)?/ 45 | end 46 | 47 | def actual_output 48 | runner.output 49 | end 50 | 51 | def some_tests_were_run 52 | "#{pluralize(number, 'test was', 'tests were')} run" 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/support/tests/bundle.rb: -------------------------------------------------------------------------------- 1 | require_relative 'filesystem' 2 | require_relative 'command_runner' 3 | require_relative 'version' 4 | 5 | module Tests 6 | class Bundle 7 | def initialize 8 | @already_updating = false 9 | @fs = Filesystem.new 10 | end 11 | 12 | def updating 13 | if already_updating? 14 | yield self 15 | return 16 | end 17 | 18 | @already_updating = true 19 | 20 | yield self 21 | 22 | @already_updating = false 23 | 24 | install_gems 25 | end 26 | 27 | def add_gem(gem, *args) 28 | updating do 29 | options = args.last.is_a?(Hash) ? args.pop : {} 30 | version = args.shift 31 | line = assemble_gem_line(gem, version, options) 32 | fs.append_to_file('Gemfile', line) 33 | end 34 | end 35 | 36 | def remove_gem(gem) 37 | updating do 38 | fs.comment_lines_matching('Gemfile', /^ *gem ("|')#{gem}\1/) 39 | end 40 | end 41 | 42 | def install_gems 43 | CommandRunner.run!('bundle install --local') do |runner| 44 | runner.retries = 5 45 | end 46 | end 47 | 48 | def version_of(gem) 49 | Version.new(Bundler.definition.specs[gem][0].version) 50 | end 51 | 52 | def includes?(gem) 53 | Bundler.definition.dependencies.any? do |dependency| 54 | dependency.name == gem 55 | end 56 | end 57 | 58 | def version 59 | Bundler::VERSION 60 | end 61 | 62 | protected 63 | 64 | attr_reader :fs 65 | 66 | private 67 | 68 | def already_updating? 69 | @already_updating 70 | end 71 | 72 | def assemble_gem_line(gem, version, options) 73 | formatted_options = options. 74 | map { |key, value| "#{key}: #{formatted_value(value)}" }. 75 | join(', ') 76 | 77 | line = %(gem '#{gem}') 78 | 79 | if version 80 | line << %(, '#{version}') 81 | end 82 | 83 | if options.any? 84 | line << %(, #{formatted_options}) 85 | end 86 | 87 | line << "\n" 88 | end 89 | 90 | def formatted_value(value) 91 | if value.is_a?(Pathname) 92 | value.to_s.inspect 93 | else 94 | value.inspect 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/support/tests/current_bundle.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | require 'appraisal' 3 | 4 | module Tests 5 | class CurrentBundle 6 | AppraisalNotSpecified = Class.new(ArgumentError) 7 | 8 | include Singleton 9 | 10 | def assert_appraisal! 11 | unless appraisal_in_use? 12 | message = <<EOT 13 | 14 | 15 | Please run tests starting with `appraisal <appraisal_name>`. 16 | Possible appraisals are: #{available_appraisals} 17 | 18 | EOT 19 | raise AppraisalNotSpecified, message 20 | end 21 | end 22 | 23 | def appraisal_in_use? 24 | path.dirname == root.join('gemfiles') 25 | end 26 | 27 | def current_or_latest_appraisal 28 | current_appraisal || latest_appraisal 29 | end 30 | 31 | def latest_appraisal 32 | available_appraisals.max 33 | end 34 | 35 | def available_appraisals 36 | Appraisal::AppraisalFile.each.map(&:name) 37 | end 38 | 39 | private 40 | 41 | def current_appraisal 42 | if appraisal_in_use? 43 | File.basename(path, '.gemfile') 44 | end 45 | end 46 | 47 | def path 48 | Bundler.default_gemfile 49 | end 50 | 51 | def root 52 | Pathname.new('../../../..').expand_path(__FILE__) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/support/tests/database.rb: -------------------------------------------------------------------------------- 1 | require_relative 'database_configuration' 2 | 3 | module Tests 4 | class Database 5 | NAME = 'shoulda-matchers-test'.freeze 6 | ADAPTER_NAME = ENV.fetch('DATABASE_ADAPTER', 'sqlite3').to_sym 7 | 8 | include Singleton 9 | 10 | attr_reader :config 11 | 12 | def initialize 13 | @config = Tests::DatabaseConfiguration.for(NAME, ADAPTER_NAME) 14 | end 15 | 16 | def name 17 | config.database 18 | end 19 | 20 | def adapter_name 21 | config.adapter 22 | end 23 | 24 | def adapter_class 25 | config.adapter_class 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/support/tests/database_adapters/config/postgresql.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: unicode 4 | pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> 5 | host: <%= ENV.fetch("DB_HOST", "localhost") %> 6 | username: <%= ENV.fetch("DB_USER", "postgres") %> 7 | password: <%= ENV.fetch("DB_USER_PASSWORD", "postgres") %> 8 | 9 | development: 10 | <<: *default 11 | database: shoulda-matchers-test_development 12 | 13 | test: 14 | <<: *default 15 | database: shoulda-matchers-test_test 16 | 17 | production: 18 | <<: *default 19 | database: shoulda-matchers-test_production 20 | -------------------------------------------------------------------------------- /spec/support/tests/database_adapters/config/sqlite3.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: sqlite3 3 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 4 | timeout: 5000 5 | 6 | development: 7 | <<: *default 8 | database: db/development.sqlite3 9 | 10 | test: 11 | <<: *default 12 | database: db/test.sqlite3 13 | 14 | production: 15 | <<: *default 16 | database: db/production.sqlite3 17 | -------------------------------------------------------------------------------- /spec/support/tests/database_adapters/postgresql.rb: -------------------------------------------------------------------------------- 1 | module Tests 2 | module DatabaseAdapters 3 | class PostgreSQL 4 | def self.name 5 | :postgresql 6 | end 7 | 8 | attr_reader :database 9 | 10 | def initialize(database) 11 | @database = database 12 | end 13 | 14 | def adapter 15 | self.class.name 16 | end 17 | 18 | def require_dependencies 19 | require 'pg' 20 | end 21 | end 22 | 23 | DatabaseConfigurationRegistry.instance.register(PostgreSQL) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/support/tests/database_adapters/sqlite3.rb: -------------------------------------------------------------------------------- 1 | module Tests 2 | module DatabaseAdapters 3 | class SQLite3 4 | def self.name 5 | :sqlite3 6 | end 7 | 8 | def initialize(_database) 9 | end 10 | 11 | def adapter 12 | self.class.name 13 | end 14 | 15 | def database 16 | 'db/db.sqlite3' 17 | end 18 | 19 | def require_dependencies 20 | require 'sqlite3' 21 | end 22 | end 23 | 24 | DatabaseConfigurationRegistry.instance.register(SQLite3) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/support/tests/database_configuration.rb: -------------------------------------------------------------------------------- 1 | require_relative 'database_configuration_registry' 2 | require 'delegate' 3 | 4 | module Tests 5 | class DatabaseConfiguration < SimpleDelegator 6 | attr_reader :adapter_class 7 | 8 | def self.for(database_name, adapter_name) 9 | config_class = DatabaseConfigurationRegistry.instance.get(adapter_name) 10 | config = config_class.new(database_name) 11 | new(config) 12 | end 13 | 14 | def initialize(config) 15 | @adapter_class = config.class.to_s.split('::').last 16 | super(config) 17 | end 18 | 19 | def load_file 20 | YAML::load_file(File.join(__dir__, "database_adapters/config/#{adapter}.yml"), aliases: true) 21 | rescue ArgumentError 22 | YAML::load_file(File.join(__dir__, "database_adapters/config/#{adapter}.yml")) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/support/tests/database_configuration_registry.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | module Tests 4 | class DatabaseConfigurationRegistry 5 | include Singleton 6 | 7 | def initialize 8 | @registry = {} 9 | end 10 | 11 | def register(config_class) 12 | registry[config_class.name] = config_class 13 | end 14 | 15 | def get(name) 16 | registry.fetch(name) do 17 | raise KeyError, "No such adapter registered: #{name}" 18 | end 19 | end 20 | 21 | protected 22 | 23 | attr_reader :registry 24 | end 25 | end 26 | 27 | require_relative 'database_adapters/postgresql' 28 | require_relative 'database_adapters/sqlite3' 29 | -------------------------------------------------------------------------------- /spec/support/tests/version.rb: -------------------------------------------------------------------------------- 1 | module Tests 2 | class Version 3 | def initialize(version) 4 | @version = Gem::Version.new(version.to_s) 5 | end 6 | 7 | def <(other_version) 8 | compare?(:<, other_version) 9 | end 10 | 11 | def <=(other_version) 12 | compare?(:<=, other_version) 13 | end 14 | 15 | def ==(other_version) 16 | compare?(:==, other_version) 17 | end 18 | 19 | def >=(other_version) 20 | compare?(:>=, other_version) 21 | end 22 | 23 | def >(other_version) 24 | compare?(:>, other_version) 25 | end 26 | 27 | def =~(other_version) 28 | Gem::Requirement.new(other_version).satisfied_by?(version) 29 | end 30 | 31 | def to_s 32 | version.to_s 33 | end 34 | 35 | protected 36 | 37 | attr_reader :version 38 | 39 | private 40 | 41 | def compare?(operator, other_version) 42 | Gem::Requirement.new("#{operator} #{other_version}").satisfied_by?(version) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/support/unit/attribute.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | class Attribute 3 | DEFAULT_COLUMN_TYPE = :string 4 | DEFAULT_COLUMN_OPTIONS = { 5 | null: false, 6 | array: false, 7 | }.freeze 8 | 9 | def initialize(args) 10 | @args = args 11 | end 12 | 13 | def name 14 | args.fetch(:name) 15 | end 16 | 17 | def column_type 18 | args.fetch(:column_type, DEFAULT_COLUMN_TYPE) 19 | end 20 | 21 | def column_options 22 | { 23 | type: column_type, 24 | options: DEFAULT_COLUMN_OPTIONS.merge(args.fetch(:column_options, {})), 25 | } 26 | end 27 | 28 | def array? 29 | column_options[:array] 30 | end 31 | 32 | def default_value 33 | args.fetch(:default_value) do 34 | if column_options[:null] 35 | nil 36 | else 37 | Shoulda::Matchers::Util.dummy_value_for(value_type, array: array?) 38 | end 39 | end 40 | end 41 | 42 | protected 43 | 44 | attr_reader :args 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/support/unit/capture.rb: -------------------------------------------------------------------------------- 1 | module Kernel 2 | # #capture, #silence_stream, and #silence_stderr were removed in rails 5.0, 3 | # but we keep it them here 4 | 5 | if method_defined?(:capture) 6 | undef_method :capture 7 | end 8 | 9 | def capture(stream) 10 | stream = stream.to_s 11 | captured_stream = Tempfile.new(stream) 12 | stream_io = eval("$#{stream}", binding, __FILE__, __LINE__) # rubocop:disable Security/Eval 13 | origin_stream = stream_io.dup 14 | stream_io.reopen(captured_stream) 15 | 16 | yield 17 | 18 | stream_io.rewind 19 | captured_stream.read 20 | ensure 21 | captured_stream.unlink 22 | stream_io.reopen(origin_stream) 23 | end 24 | 25 | if method_defined?(:silence_stream) 26 | undef_method :silence_stream 27 | end 28 | 29 | def silence_stream(stream) 30 | old_stream = stream.dup 31 | stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null') 32 | stream.sync = true 33 | yield 34 | ensure 35 | stream.reopen(old_stream) 36 | old_stream.close 37 | end 38 | 39 | if method_defined?(:silence_stderr) 40 | undef_method :silence_stderr 41 | end 42 | 43 | def silence_stderr 44 | silence_stream($stderr) { yield if block_given? } 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/support/unit/configuration.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | class Configuration 3 | CLASSES = %i[ 4 | ActiveModelHelpers 5 | ActiveModelVersions 6 | ActiveRecordVersions 7 | ClassBuilder 8 | ColumnTypeHelpers 9 | ControllerBuilder 10 | DatabaseHelpers 11 | I18nFaker 12 | MailerBuilder 13 | MessageHelpers 14 | ModelBuilder 15 | RailsVersions 16 | ValidationMatcherScenarioHelpers 17 | ].freeze 18 | 19 | def self.configure_example_groups(config) 20 | CLASSES.each do |class_name| 21 | constantized_class = "UnitTests::#{class_name}" 22 | Object.const_get(constantized_class).configure_example_group(config) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/support/unit/create_model_arguments/has_many.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module CreateModelArguments 3 | class HasMany < Basic 4 | def columns 5 | super.except(attribute_name) 6 | end 7 | 8 | private 9 | 10 | def default_attribute_name 11 | :children 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/support/unit/create_model_arguments/uniqueness_matcher.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module CreateModelArguments 3 | class UniquenessMatcher < Basic 4 | def self.normalize_attribute(attribute) 5 | if attribute.is_a?(Hash) 6 | Attribute.new(attribute) 7 | else 8 | Attribute.new(name: attribute) 9 | end 10 | end 11 | 12 | def self.normalize_attributes(attributes) 13 | attributes.map do |attribute| 14 | normalize_attribute(attribute) 15 | end 16 | end 17 | 18 | def columns 19 | attributes.inject({}) do |options, attribute| 20 | options.merge!( 21 | attribute.name => { 22 | type: attribute.column_type, 23 | options: attribute.column_options, 24 | }, 25 | ) 26 | end 27 | end 28 | 29 | def validation_options 30 | super.merge!(scope: scope_attribute_names) 31 | end 32 | 33 | def attribute_default_values_by_name 34 | attributes.inject({}) do |values, attribute| 35 | values.merge!(attribute.name => attribute.default_value) 36 | end 37 | end 38 | 39 | protected 40 | 41 | def attribute_class 42 | Attribute 43 | end 44 | 45 | private 46 | 47 | def attributes 48 | [attribute] + scope_attributes + additional_attributes 49 | end 50 | 51 | def scope_attribute_names 52 | scope_attributes.map(&:name) 53 | end 54 | 55 | def scope_attributes 56 | @_scope_attributes ||= self.class.normalize_attributes( 57 | args.fetch(:scopes, []), 58 | ) 59 | end 60 | 61 | def additional_attributes 62 | @_additional_attributes ||= self.class.normalize_attributes( 63 | args.fetch(:additional_attributes, []), 64 | ) 65 | end 66 | 67 | class Attribute < UnitTests::Attribute 68 | def value_type 69 | args.fetch(:value_type) { column_type } 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/active_model_helpers.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module ActiveModelHelpers 3 | def self.configure_example_group(example_group) 4 | example_group.include(self) 5 | end 6 | 7 | def custom_validation(options = {}, &block) 8 | attribute_name = options.fetch(:attribute_name, :attr) 9 | attribute_type = options.fetch(:attribute_type, :integer) 10 | column_options = options.fetch(:column_options, {}) 11 | attribute_options = { type: attribute_type, options: column_options } 12 | 13 | define_model(:example, attribute_name => attribute_options) do 14 | validate :custom_validation 15 | 16 | define_method(:custom_validation, &block) 17 | end.new 18 | end 19 | alias record_with_custom_validation custom_validation 20 | 21 | def validating_format(options) 22 | define_model :example, attr: :string do 23 | validates_format_of :attr, options 24 | end.new 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/active_model_versions.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module ActiveModelVersions 3 | def self.configure_example_group(example_group) 4 | example_group.include(self) 5 | example_group.extend(self) 6 | end 7 | 8 | def active_model_version 9 | Tests::Version.new(::ActiveModel::VERSION::STRING) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/active_record_versions.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module ActiveRecordVersions 3 | def self.configure_example_group(example_group) 4 | example_group.include(self) 5 | example_group.extend(self) 6 | end 7 | 8 | extend self 9 | 10 | def active_record_version 11 | Tests::Version.new(::ActiveRecord::VERSION::STRING) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/active_resource_builder.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module ActiveResourceBuilder 3 | def self.configure_example_group(example_group) 4 | require 'active_resource' 5 | 6 | example_group.include ActiveResourceBuilder 7 | 8 | example_group.after do 9 | ActiveSupport::Dependencies.clear 10 | end 11 | end 12 | 13 | def define_active_resource_class(class_name, attributes = {}, &block) 14 | define_class(class_name, ActiveResource::Base) do 15 | schema do 16 | attributes.each do |attr, type| 17 | attribute attr, type 18 | end 19 | end 20 | 21 | if block_given? 22 | class_eval(&block) 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/allow_value_matcher_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative '../record_with_different_error_attribute_builder' 2 | require_relative '../record_builder_with_i18n_validation_message' 3 | 4 | module UnitTests 5 | module AllowValueMatcherHelpers 6 | def builder_for_record_with_different_error_attribute(options = {}) 7 | RecordWithDifferentErrorAttributeBuilder.new(options) 8 | end 9 | 10 | def builder_for_record_with_unrelated_error(options = {}) 11 | RecordWithUnrelatedErrorBuilder.new(options) 12 | end 13 | 14 | def builder_for_record_with_different_error_attribute_using_i18n(options = {}) 15 | builder = builder_for_record_with_different_error_attribute(options) 16 | RecordBuilderWithI18nValidationMessage.new(builder) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/application_configuration_helpers.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module ApplicationConfigurationHelpers 3 | def with_belongs_to_as_required_by_default(&block) 4 | configuring_application( 5 | ::ActiveRecord::Base, 6 | :belongs_to_required_by_default, 7 | true, 8 | &block 9 | ) 10 | end 11 | 12 | def with_belongs_to_as_optional_by_default(&block) 13 | configuring_application( 14 | ::ActiveRecord::Base, 15 | :belongs_to_required_by_default, 16 | false, 17 | &block 18 | ) 19 | end 20 | 21 | def with_strict_loading_by_default_enabled(&block) 22 | configuring_application( 23 | ::ActiveRecord::Base, 24 | :strict_loading_by_default, 25 | true, 26 | &block 27 | ) 28 | end 29 | 30 | def with_strict_loading_by_default_disabled(&block) 31 | configuring_application( 32 | ::ActiveRecord::Base, 33 | :strict_loading_by_default, 34 | false, 35 | &block 36 | ) 37 | end 38 | 39 | private 40 | 41 | def configuring_application(config, name, value) 42 | previous_value = config.send(name) 43 | config.send("#{name}=", value) 44 | yield 45 | ensure 46 | config.send("#{name}=", previous_value) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/column_type_helpers.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module ColumnTypeHelpers 3 | def self.configure_example_group(example_group) 4 | example_group.include(self) 5 | end 6 | 7 | def column_type_class_namespace 8 | if database_adapter == :postgresql 9 | ActiveRecord::ConnectionAdapters::PostgreSQL 10 | else 11 | ActiveRecord::Type 12 | end 13 | end 14 | 15 | def column_type_class_for(type) 16 | namespace = 17 | if type == :integer && database_adapter == :postgresql 18 | column_type_class_namespace::OID 19 | else 20 | column_type_class_namespace 21 | end 22 | 23 | namespace.const_get(type.to_s.camelize) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/confirmation_matcher_helpers.rb: -------------------------------------------------------------------------------- 1 | require_relative '../record_validating_confirmation_builder' 2 | require_relative '../record_builder_with_i18n_validation_message' 3 | 4 | module UnitTests 5 | module ConfirmationMatcherHelpers 6 | def builder_for_record_validating_confirmation(options = {}) 7 | RecordValidatingConfirmationBuilder.new(options) 8 | end 9 | 10 | def builder_for_record_validating_confirmation_with_18n_message(options = {}) 11 | builder = builder_for_record_validating_confirmation(options) 12 | RecordBuilderWithI18nValidationMessage.new( 13 | builder, 14 | validation_message_key: :confirmation, 15 | ) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/controller_builder.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module ControllerBuilder 3 | def self.configure_example_group(example_group) 4 | example_group.include(self) 5 | 6 | example_group.after do 7 | delete_temporary_views 8 | restore_original_routes 9 | end 10 | end 11 | 12 | def define_controller(class_name, &block) 13 | new_class_name = if class_name.to_s =~ /Controller$/ 14 | class_name.to_s 15 | else 16 | "#{class_name}Controller" 17 | end 18 | 19 | define_class(new_class_name, ActionController::Base, &block) 20 | end 21 | 22 | def define_routes(&block) 23 | self.routes = $test_app.draw_routes(&block) 24 | end 25 | 26 | def build_fake_response(opts = {}, &block) 27 | action = opts[:action] || 'example' 28 | partial = opts[:partial] || '_partial' 29 | block ||= lambda { head :ok } 30 | controller_class = define_controller('Examples') do 31 | layout false 32 | define_method(action, &block) 33 | end 34 | controller_class.view_paths = [$test_app.temp_views_directory.to_s] 35 | 36 | define_routes do 37 | get 'examples', to: "examples##{action}" 38 | end 39 | 40 | create_view("examples/#{action}.html.erb", 'action') 41 | create_view("examples/#{partial}.html.erb", 'partial') 42 | 43 | setup_rails_controller_test(controller_class) 44 | self.class.render_views(true) 45 | 46 | get action 47 | 48 | controller 49 | end 50 | 51 | def setup_rails_controller_test(controller_class) 52 | @controller = controller_class.new 53 | end 54 | 55 | def create_view(path, contents) 56 | $test_app.create_temp_view(path, contents) 57 | end 58 | 59 | def delete_temporary_views 60 | $test_app.delete_temp_views 61 | end 62 | 63 | def restore_original_routes 64 | Rails.application.reload_routes! 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/database_helpers.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module DatabaseHelpers 3 | def self.configure_example_group(example_group) 4 | example_group.include(self) 5 | example_group.extend(self) 6 | end 7 | 8 | extend self 9 | 10 | def database_adapter 11 | Tests::Database.instance.adapter_name 12 | end 13 | 14 | def postgresql? 15 | database_adapter == :postgresql 16 | end 17 | 18 | alias :database_supports_array_columns? :postgresql? 19 | alias :database_supports_uuid_columns? :postgresql? 20 | alias :database_supports_money_columns? :postgresql? 21 | alias :database_supports_expression_indexes? :postgresql? 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/i18n_faker.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module I18nFaker 3 | extend self 4 | 5 | def self.configure_example_group(example_group) 6 | example_group.include(self) 7 | end 8 | 9 | def stubbing_translations(translations) 10 | stub_translations(translations) 11 | yield 12 | ensure 13 | I18n.backend.reload! 14 | I18n.backend.send(:init_translations) 15 | end 16 | 17 | def stub_translations(translations) 18 | translations.each do |key, message| 19 | stub_translation(key, message) 20 | end 21 | end 22 | 23 | def stub_translation(key_or_keys, message) 24 | keys = [key_or_keys].flatten.join('.').split('.') 25 | tree = keys.reverse.inject(message) { |data, key| { key => data } } 26 | I18n.backend.store_translations(:en, tree) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/mailer_builder.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module MailerBuilder 3 | def self.configure_example_group(example_group) 4 | example_group.include(self) 5 | end 6 | 7 | def define_mailer(name, _paths, &block) 8 | class_name = name.to_s.pluralize.classify 9 | define_class(class_name, ActionMailer::Base, &block) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/message_helpers.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module MessageHelpers 3 | include Shoulda::Matchers::WordWrap 4 | 5 | def self.configure_example_group(example_group) 6 | example_group.include(self) 7 | end 8 | 9 | def format_message(message, one_line: false) 10 | stripped_message = message.strip_heredoc.strip 11 | 12 | if one_line 13 | stripped_message.tr("\n", ' ').squeeze(' ') 14 | else 15 | word_wrap(stripped_message) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/rails_versions.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module RailsVersions 3 | extend self 4 | 5 | def self.configure_example_group(example_group) 6 | example_group.include(self) 7 | example_group.extend(self) 8 | end 9 | 10 | def rails_version 11 | Tests::Version.new(Rails::VERSION::STRING) 12 | end 13 | 14 | def rails_oldest_version_supported 15 | 6.1 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/support/unit/helpers/validation_matcher_scenario_helpers.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module ValidationMatcherScenarioHelpers 3 | def self.configure_example_group(example_group) 4 | example_group.include(self) 5 | end 6 | 7 | def build_scenario_for_validation_matcher(args) 8 | UnitTests::ValidationMatcherScenario.new( 9 | build_validation_matcher_scenario_args(args), 10 | ) 11 | end 12 | 13 | protected 14 | 15 | def validation_matcher_scenario_args 16 | {} 17 | end 18 | 19 | def configure_validation_matcher(matcher) 20 | matcher 21 | end 22 | 23 | private 24 | 25 | def build_validation_matcher_scenario_args(args) 26 | args. 27 | deep_merge(validation_matcher_scenario_args). 28 | deep_merge( 29 | matcher_name: matcher_name, 30 | matcher_proc: method(matcher_name), 31 | ) 32 | end 33 | 34 | def matcher_name 35 | validation_matcher_scenario_args.fetch(:matcher_name) do 36 | raise KeyNotFoundError.new(<<-MESSAGE) 37 | Please implement #validation_matcher_scenario_args in your example 38 | group, in such a way that it returns a hash that contains a 39 | :matcher_name key. 40 | MESSAGE 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/support/unit/i18n.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.after do 3 | # Clear any translations added during tests by telling the backend to 4 | # replace its translations with whatever is in the YAML files. 5 | I18n.backend.reload! 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/unit/load_environment.rb: -------------------------------------------------------------------------------- 1 | require_relative '../tests/current_bundle' 2 | require_relative 'rails_application' 3 | 4 | Tests::CurrentBundle.instance.assert_appraisal! 5 | 6 | $test_app = UnitTests::RailsApplication.new 7 | $test_app.create 8 | $test_app.load 9 | 10 | require 'active_record/base' 11 | 12 | ENV['RAILS_ENV'] = 'test' 13 | -------------------------------------------------------------------------------- /spec/support/unit/matchers/deprecate.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module Matchers 3 | def deprecate(old_method, new_method) 4 | DeprecateMatcher.new(old_method, new_method) 5 | end 6 | 7 | class DeprecateMatcher 8 | def initialize(old_method, new_method) 9 | @old_method = old_method 10 | @new_method = new_method 11 | end 12 | 13 | def matches?(block) 14 | @captured_stderr = capture(:stderr, &block).gsub(/\n+/, ' ') 15 | captured_stderr.include?(expected_message) 16 | end 17 | 18 | def failure_message 19 | "Expected block to #{expectation}, but it did not.\nActual warning: #{actual_warning}" 20 | end 21 | alias_method :failure_message_for_should, :failure_message 22 | 23 | def failure_message_when_negated 24 | "Expected block not to #{expectation}, but it did." 25 | end 26 | alias_method :failure_message_for_should_not, 27 | :failure_message_when_negated 28 | 29 | def description 30 | "should #{expectation}" 31 | end 32 | 33 | def supports_block_expectations? 34 | true 35 | end 36 | 37 | protected 38 | 39 | attr_reader :old_method, :new_method, :captured_stderr 40 | 41 | private 42 | 43 | def expected_message 44 | "#{old_method} is deprecated and will be removed in the next major release. Please use #{new_method} instead." 45 | end 46 | 47 | def expectation 48 | "print a warning deprecating #{old_method} in favor of #{new_method}" 49 | end 50 | 51 | def actual_warning 52 | if captured_stderr.empty? 53 | 'nothing' 54 | else 55 | "\n #{captured_stderr}" 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/support/unit/matchers/fail_with_message_including_matcher.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module Matchers 3 | extend RSpec::Matchers::DSL 4 | 5 | matcher :fail_with_message_including do |expected| 6 | def supports_block_expectations? 7 | true 8 | end 9 | 10 | match do |block| 11 | @actual = nil 12 | 13 | begin 14 | block.call 15 | rescue RSpec::Expectations::ExpectationNotMetError => e 16 | @actual = e.message 17 | end 18 | 19 | @actual&.include?(expected) 20 | end 21 | 22 | def failure_message 23 | lines = ['Expectation should have failed with message including:'] 24 | lines << Shoulda::Matchers::Util.indent(expected, 2) 25 | 26 | if @actual 27 | lines << 'The full message was:' 28 | lines << Shoulda::Matchers::Util.indent(@actual, 2) 29 | else 30 | lines << 'However, the expectation did not fail at all.' 31 | end 32 | 33 | lines.join("\n") 34 | end 35 | 36 | def failure_message_for_should 37 | failure_message 38 | end 39 | 40 | def failure_message_when_negated 41 | lines = ['Expectation should not have failed with message including:'] 42 | lines << Shoulda::Matchers::Util.indent(expected, 2) 43 | lines.join("\n") 44 | end 45 | 46 | def failure_message_for_should_not 47 | failure_message_when_negated 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/support/unit/matchers/fail_with_message_matcher.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module Matchers 3 | extend RSpec::Matchers::DSL 4 | 5 | matcher :fail_with_message do |raw_expected, wrap: false| 6 | expected = 7 | if wrap 8 | Shoulda::Matchers.word_wrap(raw_expected) 9 | else 10 | raw_expected 11 | end 12 | 13 | def supports_block_expectations? 14 | true 15 | end 16 | 17 | match do |block| 18 | @actual = nil 19 | 20 | begin 21 | block.call 22 | rescue RSpec::Expectations::ExpectationNotMetError => e 23 | @actual = e.message 24 | end 25 | 26 | @actual && @actual == expected.sub(/\n\z/, '') 27 | end 28 | 29 | define_method :failure_message do 30 | lines = ['Expectation should have failed with message:'] 31 | lines << Shoulda::Matchers::Util.indent(expected, 2) 32 | 33 | if @actual 34 | diff = differ.diff(@actual, expected)[1..] 35 | 36 | lines << 'Actually failed with:' 37 | lines << Shoulda::Matchers::Util.indent(@actual, 2) 38 | 39 | if diff 40 | lines << 'Diff:' 41 | lines << Shoulda::Matchers::Util.indent(diff, 2) 42 | end 43 | else 44 | lines << 'However, the expectation did not fail at all.' 45 | end 46 | 47 | lines.join("\n") 48 | end 49 | 50 | define_method :failure_message_when_negated do 51 | lines = ['Expectation should not have failed with message:'] 52 | lines << Shoulda::Matchers::Util.indent(expected, 2) 53 | lines.join("\n") 54 | end 55 | 56 | private 57 | 58 | def differ 59 | @_differ ||= RSpec::Support::Differ.new 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/support/unit/matchers/print_warning_including.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module Matchers 3 | def print_warning_including(expected_warning) 4 | PrintWarningIncludingMatcher.new(expected_warning) 5 | end 6 | 7 | class PrintWarningIncludingMatcher 8 | def initialize(expected_warning) 9 | @expected_warning = collapse_whitespace(expected_warning) 10 | end 11 | 12 | def matches?(block) 13 | @captured_stderr = collapse_whitespace(capture(:stderr, &block)) 14 | @was_negated = false 15 | captured_stderr.include?(expected_warning) 16 | end 17 | 18 | def does_not_match?(block) 19 | !matches?(block).tap do 20 | @was_negated = true 21 | end 22 | end 23 | 24 | def failure_message 25 | "Expected block to #{expectation}\n\nHowever, #{aberration}" 26 | end 27 | 28 | def failure_message_when_negated 29 | "Expected block not to #{expectation}\n\nHowever, #{aberration}" 30 | end 31 | 32 | def description 33 | "should print a warning containing #{expected_warning.inspect}" 34 | end 35 | 36 | def supports_block_expectations? 37 | true 38 | end 39 | 40 | private 41 | 42 | attr_reader :expected_warning, :captured_stderr 43 | 44 | def was_negated? 45 | @was_negated 46 | end 47 | 48 | def expectation 49 | "print a warning containing:\n\n #{expected_warning}" 50 | end 51 | 52 | def aberration 53 | if was_negated? 54 | 'it did.' 55 | elsif captured_stderr.empty? 56 | 'it actually printed nothing.' 57 | else 58 | "it actually printed:\n\n #{captured_stderr}" 59 | end 60 | end 61 | 62 | def collapse_whitespace(string) 63 | string.gsub(/\n+/, ' ').squeeze(' ') 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/support/unit/model_creators.rb: -------------------------------------------------------------------------------- 1 | module UnitTests 2 | module ModelCreators 3 | class << self 4 | def register(name, klass) 5 | registrations[name] = klass 6 | end 7 | 8 | def retrieve(name) 9 | registrations[name] 10 | end 11 | 12 | private 13 | 14 | def registrations 15 | @_registrations ||= {} 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/unit/model_creators/active_model.rb: -------------------------------------------------------------------------------- 1 | require_relative '../model_creators' 2 | require 'forwardable' 3 | 4 | module UnitTests 5 | module ModelCreators 6 | class ActiveModel 7 | def self.call(args) 8 | new(args).call 9 | end 10 | 11 | extend Forwardable 12 | 13 | def_delegators( 14 | :arguments, 15 | :attribute_name, 16 | :attribute_default_values_by_name, 17 | ) 18 | 19 | def initialize(args) 20 | @arguments = CreateModelArguments::Basic.wrap( 21 | args.merge!( 22 | model_creation_strategy: UnitTests::ModelCreationStrategies::ActiveModel, 23 | ), 24 | ) 25 | @model_creator = Basic.new(arguments) 26 | end 27 | 28 | def call 29 | model_creator.call 30 | end 31 | 32 | protected 33 | 34 | attr_reader :arguments, :model_creator 35 | end 36 | 37 | register(:active_model, ActiveModel) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/support/unit/model_creators/active_record.rb: -------------------------------------------------------------------------------- 1 | require_relative '../model_creators' 2 | require 'forwardable' 3 | 4 | module UnitTests 5 | module ModelCreators 6 | class ActiveRecord 7 | def self.call(args) 8 | new(args).call 9 | end 10 | 11 | extend Forwardable 12 | 13 | def_delegators( 14 | :arguments, 15 | :attribute_default_values_by_name, 16 | :attribute_name, 17 | :model_name, 18 | ) 19 | 20 | def_delegators :model_creator, :customize_model 21 | 22 | def initialize(args) 23 | @arguments = CreateModelArguments::Basic.wrap( 24 | args.merge!( 25 | model_creation_strategy: UnitTests::ModelCreationStrategies::ActiveRecord, 26 | ), 27 | ) 28 | @model_creator = Basic.new(arguments) 29 | end 30 | 31 | def call 32 | model_creator.call 33 | end 34 | 35 | protected 36 | 37 | attr_reader :arguments, :model_creator 38 | end 39 | 40 | register(:active_record, ActiveRecord) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/support/unit/model_creators/active_record/has_many.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../model_creators' 2 | require 'forwardable' 3 | 4 | module UnitTests 5 | module ModelCreators 6 | class ActiveRecord 7 | class HasMany 8 | def self.call(args) 9 | new(args).call 10 | end 11 | 12 | extend Forwardable 13 | 14 | def_delegators( 15 | :arguments, 16 | :attribute_name, 17 | :attribute_default_values_by_name, 18 | ) 19 | 20 | def initialize(args) 21 | @arguments = CreateModelArguments::HasMany.wrap(args) 22 | end 23 | 24 | def call 25 | child_model_creator.call 26 | parent_model_creator.call 27 | end 28 | 29 | protected 30 | 31 | attr_reader :arguments 32 | 33 | private 34 | 35 | alias_method :association_name, :attribute_name 36 | alias_method :parent_model_creator_arguments, :arguments 37 | 38 | def child_model_creator 39 | @_child_model_creator ||= 40 | UnitTests::ModelCreationStrategies::ActiveRecord.new( 41 | child_model_name, 42 | ) 43 | end 44 | 45 | def parent_model_creator 46 | @_parent_model_creator ||= begin 47 | model_creator = UnitTests::ModelCreators::ActiveRecord.new( 48 | parent_model_creator_arguments, 49 | ) 50 | 51 | model_creator.customize_model do |model| 52 | model.has_many(association_name) 53 | end 54 | 55 | model_creator 56 | end 57 | end 58 | 59 | def child_model_name 60 | association_name.to_s.classify 61 | end 62 | end 63 | end 64 | 65 | register(:"active_record/has_many", ActiveRecord::HasMany) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/support/unit/model_creators/active_record/uniqueness_matcher.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../model_creators' 2 | require 'forwardable' 3 | 4 | module UnitTests 5 | module ModelCreators 6 | class ActiveRecord 7 | class UniquenessMatcher 8 | def self.call(args) 9 | new(args).call 10 | end 11 | 12 | extend Forwardable 13 | 14 | def_delegators( 15 | :arguments, 16 | :attribute_name, 17 | :attribute_default_values_by_name, 18 | ) 19 | 20 | def initialize(args) 21 | @arguments = CreateModelArguments::UniquenessMatcher.wrap(args) 22 | @model_creator = UnitTests::ModelCreators::ActiveRecord.new( 23 | arguments, 24 | ) 25 | end 26 | 27 | def call 28 | model_creator.call 29 | end 30 | 31 | protected 32 | 33 | attr_reader :arguments, :model_creator 34 | end 35 | end 36 | 37 | register( 38 | :"active_record/uniqueness_matcher", 39 | ActiveRecord::UniquenessMatcher, 40 | ) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/support/unit/record_builder_with_i18n_validation_message.rb: -------------------------------------------------------------------------------- 1 | require 'delegate' 2 | 3 | module UnitTests 4 | class RecordBuilderWithI18nValidationMessage < SimpleDelegator 5 | def initialize(builder, options = {}) 6 | super(builder) 7 | @options = default_options.merge!(options) 8 | builder.message = validation_message_key 9 | end 10 | 11 | def validation_message_key 12 | options[:validation_message_key] 13 | end 14 | 15 | protected 16 | 17 | attr_reader :builder, :options 18 | 19 | private 20 | 21 | def model 22 | @_model ||= super.tap do |_model| 23 | stub_validation_messages 24 | end 25 | end 26 | 27 | def stub_validation_messages 28 | stub_default_validation_message 29 | stub_attribute_specific_validation_message 30 | end 31 | 32 | def stub_default_validation_message 33 | keys = [ 34 | 'activerecord.errors.messages', 35 | validation_message_key, 36 | ] 37 | 38 | I18nFaker.stub_translation(keys, default_message) 39 | end 40 | 41 | def stub_attribute_specific_validation_message 42 | keys = [ 43 | 'activerecord.errors', 44 | "models.#{builder.model_name.to_s.underscore}", 45 | "attributes.#{builder.attribute_that_receives_error}", 46 | validation_message_key, 47 | ] 48 | 49 | I18nFaker.stub_translation( 50 | keys, 51 | message_for_attribute_that_receives_error, 52 | ) 53 | end 54 | 55 | def default_message 56 | 'the wrong message' 57 | end 58 | 59 | def message_for_attribute_that_receives_error 60 | 'the right message' 61 | end 62 | 63 | def default_options 64 | { 65 | validation_message_key: :validation_message_key, 66 | } 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/support/unit/record_validating_confirmation_builder.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helpers/model_builder' 2 | 3 | module UnitTests 4 | class RecordValidatingConfirmationBuilder 5 | include ModelBuilder 6 | 7 | def initialize(options) 8 | @options = options 9 | end 10 | 11 | def model 12 | @_model ||= create_model 13 | end 14 | 15 | def model_name 16 | options.fetch(:model_name, 'Example') 17 | end 18 | 19 | def record 20 | model.new 21 | end 22 | 23 | def message=(message) 24 | options[:message] = message 25 | end 26 | 27 | def attribute_to_confirm 28 | options.fetch(:attribute, :attribute_to_confirm) 29 | end 30 | 31 | def confirmation_attribute 32 | options.fetch( 33 | :confirmation_attribute, 34 | :"#{attribute_to_confirm}_confirmation", 35 | ) 36 | end 37 | 38 | def attribute_that_receives_error 39 | confirmation_attribute 40 | end 41 | 42 | protected 43 | 44 | attr_reader :options 45 | 46 | private 47 | 48 | def create_model 49 | define_model(model_name, attribute_to_confirm => :string) do |model| 50 | model.validates_confirmation_of(attribute_to_confirm, options) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/support/unit/record_with_different_error_attribute_builder.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helpers/model_builder' 2 | 3 | module UnitTests 4 | class RecordWithDifferentErrorAttributeBuilder 5 | include ModelBuilder 6 | 7 | def initialize(options) 8 | @options = options.reverse_merge(default_options) 9 | end 10 | 11 | def attribute_that_receives_error 12 | options[:attribute_that_receives_error] 13 | end 14 | 15 | def attribute_to_validate 16 | options[:attribute_to_validate] 17 | end 18 | 19 | def message 20 | options[:message] 21 | end 22 | 23 | def message=(message) 24 | options[:message] = message 25 | end 26 | 27 | def model 28 | @_model ||= create_model 29 | end 30 | 31 | def model_name 32 | 'Example' 33 | end 34 | 35 | def record 36 | model.new 37 | end 38 | 39 | def valid_value 40 | 'some value' 41 | end 42 | 43 | protected 44 | 45 | attr_reader :options 46 | 47 | private 48 | 49 | def context 50 | { 51 | validation_method_name: validation_method_name, 52 | valid_value: valid_value, 53 | attribute_to_validate: attribute_to_validate, 54 | attribute_that_receives_error: attribute_that_receives_error, 55 | message: message, 56 | } 57 | end 58 | 59 | def create_model 60 | _context = context 61 | 62 | define_model model_name, model_columns do 63 | validate _context[:validation_method_name] 64 | 65 | define_method(_context[:validation_method_name]) do 66 | if self[_context[:attribute_to_validate]] != _context[:valid_value] 67 | errors.add(_context[:attribute_that_receives_error], _context[:message]) 68 | end 69 | end 70 | end 71 | end 72 | 73 | def validation_method_name 74 | :custom_validation 75 | end 76 | 77 | def model_columns 78 | { 79 | attribute_to_validate => :string, 80 | attribute_that_receives_error => :string, 81 | } 82 | end 83 | 84 | def default_options 85 | { 86 | attribute_that_receives_error: :attribute_that_receives_error, 87 | attribute_to_validate: :attribute_to_validate, 88 | message: 'some message', 89 | } 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/support/unit/record_with_unrelated_error_builder.rb: -------------------------------------------------------------------------------- 1 | require_relative 'helpers/model_builder' 2 | 3 | module UnitTests 4 | class RecordWithUnrelatedErrorBuilder 5 | include ModelBuilder 6 | 7 | def initialize(options) 8 | @options = options.reverse_merge(default_options) 9 | end 10 | 11 | def attribute_that_receives_error 12 | options[:attribute_that_receives_error] 13 | end 14 | 15 | def attribute_to_validate 16 | options[:attribute_to_validate] 17 | end 18 | 19 | def message 20 | options[:message] 21 | end 22 | 23 | def message=(message) 24 | options[:message] = message 25 | end 26 | 27 | def model 28 | @_model ||= create_model 29 | end 30 | 31 | def model_name 32 | 'Example' 33 | end 34 | 35 | def record 36 | model.new 37 | end 38 | 39 | def valid_value 40 | 'some value' 41 | end 42 | 43 | protected 44 | 45 | attr_reader :options 46 | 47 | private 48 | 49 | def context 50 | { 51 | validation_method_name: validation_method_name, 52 | valid_value: valid_value, 53 | attribute_to_validate: attribute_to_validate, 54 | attribute_that_receives_error: attribute_that_receives_error, 55 | message: message, 56 | } 57 | end 58 | 59 | def create_model 60 | _context = context 61 | 62 | define_model model_name, model_columns do 63 | validate _context[:validation_method_name] 64 | 65 | define_method(_context[:validation_method_name]) do 66 | errors.add(_context[:attribute_that_receives_error], _context[:message]) 67 | end 68 | end 69 | end 70 | 71 | def validation_method_name 72 | :custom_validation 73 | end 74 | 75 | def model_columns 76 | { 77 | attribute_to_validate => :string, 78 | attribute_that_receives_error => :string, 79 | } 80 | end 81 | 82 | def default_options 83 | { 84 | attribute_that_receives_error: :attribute_that_receives_error, 85 | attribute_to_validate: :attribute_to_validate, 86 | message: 'some message', 87 | } 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/support/unit/shared_examples/numerical_submatcher.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'a numerical submatcher' do 2 | it 'implements the with_message method' do 3 | expect(subject).to respond_to(:with_message).with(1).arguments 4 | end 5 | 6 | it 'implements the matches? method' do 7 | expect(subject).to respond_to(:matches?).with(1).arguments 8 | end 9 | 10 | it 'implements the failure_message method' do 11 | expect(subject).to respond_to(:failure_message).with(0).arguments 12 | end 13 | 14 | it 'implements the failure_message_when_negated method' do 15 | expect(subject).to respond_to(:failure_message_when_negated).with(0).arguments 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/support/unit/validation_matcher_scenario.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module UnitTests 4 | class ValidationMatcherScenario 5 | extend Forwardable 6 | 7 | def initialize(arguments) 8 | @arguments = arguments.dup 9 | @matcher_proc = @arguments.delete(:matcher_proc) 10 | 11 | @specified_model_creator = @arguments.delete(:model_creator) do 12 | raise KeyError.new(<<-MESSAGE) 13 | :model_creator is missing. You can either provide it as an option or as 14 | a method. 15 | MESSAGE 16 | end 17 | 18 | @model_creator = model_creator_class.new(@arguments) 19 | end 20 | 21 | def record 22 | @_record ||= model.new.tap do |record| 23 | attribute_default_values_by_name.each do |attribute_name, default_value| 24 | record.public_send("#{attribute_name}=", default_value) 25 | end 26 | end 27 | end 28 | 29 | def model 30 | @_model ||= model_creator.call 31 | end 32 | 33 | def matcher 34 | @_matcher ||= matcher_proc.call(attribute_name) 35 | end 36 | 37 | protected 38 | 39 | attr_reader( 40 | :arguments, 41 | :existing_value, 42 | :matcher_proc, 43 | :model_creator, 44 | :specified_model_creator, 45 | ) 46 | 47 | private 48 | 49 | def_delegators( 50 | :model_creator, 51 | :attribute_name, 52 | :attribute_default_values_by_name, 53 | ) 54 | 55 | def model_creator_class 56 | UnitTests::ModelCreators.retrieve(specified_model_creator) || 57 | specified_model_creator 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/unit/shoulda/matchers/action_controller/filter_param_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'unit_spec_helper' 2 | 3 | describe Shoulda::Matchers::ActionController::FilterParamMatcher, type: :controller do 4 | it 'accepts filtering a filtered parameter' do 5 | filter(:secret) 6 | 7 | expect(nil).to filter_param(:secret) 8 | end 9 | 10 | it 'accepts filtering a parameter matching a filtered regex' do 11 | filter(/(?!tip)pin(?!g)/) 12 | 13 | expect(nil).to filter_param(:pin) 14 | end 15 | 16 | it 'rejects filtering an unfiltered parameter' do 17 | filter(:secret) 18 | matcher = filter_param(:other) 19 | 20 | expect(matcher.matches?(nil)).to eq false 21 | 22 | expect(matcher.failure_message).to match(/Expected other to be filtered.*secret/) 23 | end 24 | 25 | def filter(param) 26 | Rails.application.config.filter_parameters = [param] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/unit/shoulda/matchers/action_controller/redirect_to_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'unit_spec_helper' 2 | 3 | describe Shoulda::Matchers::ActionController::RedirectToMatcher, type: :controller do 4 | context 'a controller that redirects' do 5 | it 'accepts redirecting to that url' do 6 | expect(controller_redirecting_to('/some/url')).to redirect_to('/some/url') 7 | end 8 | 9 | it 'rejects redirecting to a different url' do 10 | expect(controller_redirecting_to('/some/url')). 11 | not_to redirect_to('/some/other/url') 12 | end 13 | 14 | it 'accepts redirecting to that url in a block' do 15 | expect(controller_redirecting_to('/some/url')). 16 | to redirect_to('somewhere') { '/some/url' } 17 | end 18 | 19 | it 'rejects redirecting to a different url in a block' do 20 | expect(controller_redirecting_to('/some/url')). 21 | not_to redirect_to('somewhere else') { '/some/other/url' } 22 | end 23 | 24 | def controller_redirecting_to(url) 25 | build_fake_response { redirect_to url } 26 | end 27 | end 28 | 29 | context 'a controller that does not redirect' do 30 | it 'rejects redirecting to a url' do 31 | controller = build_fake_response { render text: 'hello' } 32 | 33 | expect(controller).not_to redirect_to('/some/url') 34 | end 35 | end 36 | 37 | it 'provides the correct description when provided a block' do 38 | matcher = redirect_to('somewhere else') { '/some/other/url' } 39 | 40 | expect(matcher.description).to eq 'redirect to "somewhere else"' 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/unit/shoulda/matchers/action_controller/respond_with_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'unit_spec_helper' 2 | 3 | describe Shoulda::Matchers::ActionController::RespondWithMatcher, type: :controller do 4 | statuses = { success: 200, redirect: 301, missing: 404, error: 500, 5 | not_implemented: 501, } 6 | 7 | statuses.each do |human_name, numeric_code| 8 | context "a controller responding with #{human_name}" do 9 | it 'accepts responding with a numeric response code' do 10 | expect(controller_with_status(numeric_code)).to respond_with(numeric_code) 11 | end 12 | 13 | it 'accepts responding with a symbol response code' do 14 | expect(controller_with_status(numeric_code)).to respond_with(human_name) 15 | end 16 | 17 | it 'rejects responding with another status' do 18 | another_status = statuses.except(human_name).keys.first 19 | 20 | expect(controller_with_status(numeric_code)). 21 | not_to respond_with(another_status) 22 | end 23 | end 24 | end 25 | 26 | def controller_with_status(status) 27 | build_fake_response do 28 | render text: 'text', status: status 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/unit/shoulda/matchers/action_controller/route_params_spec.rb: -------------------------------------------------------------------------------- 1 | require 'unit_spec_helper' 2 | 3 | describe Shoulda::Matchers::ActionController::RouteParams, type: :controller do 4 | describe '#normalize' do 5 | context 'when the route parameters is a hash' do 6 | it 'stringifies the values in the hash' do 7 | expect(build_route_params(controller: :examples, action: 'example', id: '1').normalize). 8 | to eq({ controller: 'examples', action: 'example', id: '1' }) 9 | end 10 | end 11 | 12 | context 'when the route parameters is a string and a hash' do 13 | it 'produces a hash of route parameters' do 14 | expect(build_route_params('examples#example', id: '1').normalize). 15 | to eq({ controller: 'examples', action: 'example', id: '1' }) 16 | end 17 | end 18 | 19 | context 'when the route params is a string' do 20 | it 'produces a hash of route params' do 21 | expect(build_route_params('examples#index').normalize). 22 | to eq({ controller: 'examples', action: 'index' }) 23 | end 24 | end 25 | end 26 | 27 | def build_route_params(*params) 28 | Shoulda::Matchers::ActionController::RouteParams.new(params) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/unit/shoulda/matchers/action_controller/set_flash_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'unit_spec_helper' 2 | 3 | describe Shoulda::Matchers::ActionController::SetFlashMatcher, type: :controller do 4 | it_behaves_like 'set session or flash matcher' do 5 | def store_name 6 | 'flash' 7 | end 8 | 9 | def set_store 10 | set_flash 11 | end 12 | 13 | def store_within(controller) 14 | controller.flash 15 | end 16 | end 17 | 18 | it_behaves_like 'set session or flash matcher' do 19 | def store_name 20 | 'flash.now' 21 | end 22 | 23 | def set_store 24 | set_flash.now 25 | end 26 | 27 | def store_within(controller) 28 | controller.flash.now 29 | end 30 | end 31 | 32 | context 'when the controller sets both flash and flash.now' do 33 | it 'does not mix flash and flash.now' do 34 | controller = build_fake_response do 35 | flash['key for flash'] = 'value for flash' 36 | flash.now['key for flash.now'] = 'value for flash.now' 37 | end 38 | 39 | expect(controller).not_to set_flash['key for flash.now'] 40 | expect(controller).not_to set_flash.now['key for flash'] 41 | end 42 | end 43 | 44 | context 'when the now qualifier is called after the key is set' do 45 | it 'raises a QualifierOrderError' do 46 | controller = build_fake_response 47 | 48 | usage = lambda do 49 | expect(controller).to set_flash['any key'].now 50 | end 51 | 52 | expect(&usage).to raise_error(described_class::QualifierOrderError) 53 | end 54 | end 55 | 56 | context 'when the now qualifier is called after the to qualifier' do 57 | it 'raises a QualifierOrderError' do 58 | controller = build_fake_response 59 | 60 | usage = lambda do 61 | expect(controller).to set_flash.to('any value').now 62 | end 63 | 64 | expect(&usage).to raise_error(described_class::QualifierOrderError) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/unit/shoulda/matchers/action_controller/set_session_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'unit_spec_helper' 2 | 3 | describe Shoulda::Matchers::ActionController::SetSessionMatcher, type: :controller do 4 | it_behaves_like 'set session or flash matcher' do 5 | def store_name 6 | 'session' 7 | end 8 | 9 | def set_store 10 | set_session 11 | end 12 | 13 | def store_within(controller) 14 | controller.session 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/unit/shoulda/matchers/active_model/have_secure_password_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'unit_spec_helper' 2 | 3 | describe Shoulda::Matchers::ActiveModel::HaveSecurePasswordMatcher, type: :model do 4 | context 'with no arguments passed to has_secure_password' do 5 | it 'matches when the subject configures has_secure_password with default options' do 6 | working_model = define_model(:example, password_digest: :string) { has_secure_password } 7 | expect(working_model.new).to have_secure_password 8 | end 9 | 10 | it 'does not match when the subject does not authenticate a password' do 11 | no_secure_password = define_model(:example) 12 | expect(no_secure_password.new).not_to have_secure_password 13 | end 14 | 15 | it 'does not match when the subject is missing the password_digest attribute' do 16 | no_digest_column = define_model(:example) { has_secure_password } 17 | expect(no_digest_column.new).not_to have_secure_password 18 | end 19 | end 20 | 21 | context 'when custom attribute is given to has_secure_password' do 22 | it 'matches when the subject configures has_secure_password with correct options' do 23 | working_model = define_model(:example, reset_password_digest: :string) { has_secure_password :reset_password } 24 | expect(working_model.new).to have_secure_password :reset_password 25 | end 26 | 27 | it 'does not match when the subject does not authenticate a password' do 28 | no_secure_password = define_model(:example) 29 | expect(no_secure_password.new).not_to have_secure_password :reset_password 30 | end 31 | 32 | it 'does not match when the subject is missing the custom digest attribute' do 33 | no_digest_column = define_model(:example) { has_secure_password :reset_password } 34 | expect(no_digest_column.new).not_to have_secure_password :reset_password 35 | end 36 | 37 | it 'rejects with an appropriate failure message' do 38 | working_model = define_model(:example, reset_password_digest: :string) { has_secure_password :reset_password } 39 | assertion = lambda do 40 | expect(working_model.new).not_to have_secure_password :reset_password 41 | end 42 | 43 | message = <<-MESSAGE 44 | expected Example to not have a secure password, defined on reset_password attribute! 45 | MESSAGE 46 | 47 | expect(&assertion).to fail_with_message(message) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/unit/shoulda/matchers/active_record/have_readonly_attributes_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'unit_spec_helper' 2 | 3 | describe Shoulda::Matchers::ActiveRecord::HaveReadonlyAttributeMatcher, type: :model do 4 | context 'a read-only attribute' do 5 | it 'accepts' do 6 | expect(with_readonly_attr).to have_readonly_attribute(:attr) 7 | end 8 | end 9 | 10 | context 'an attribute that is not part of the read-only set' do 11 | it 'rejects being read-only' do 12 | model = define_model :example, attr: :string, other: :string do 13 | attr_readonly :attr 14 | end.new 15 | 16 | expect(model).not_to have_readonly_attribute(:other) 17 | end 18 | end 19 | 20 | context 'an attribute on a class with no readonly attributes' do 21 | it 'rejects being read-only' do 22 | expect(define_model(:example, attr: :string).new). 23 | not_to have_readonly_attribute(:attr) 24 | end 25 | 26 | it 'assigns a failure message' do 27 | model = define_model(:example, attr: :string).new 28 | matcher = have_readonly_attribute(:attr) 29 | 30 | matcher.matches?(model) 31 | 32 | expect(matcher.failure_message).not_to be_nil 33 | end 34 | end 35 | 36 | def with_readonly_attr 37 | define_model :example, attr: :string do 38 | attr_readonly :attr 39 | end.new 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/unit/shoulda/matchers/doublespeak/double_implementation_registry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'doublespeak_spec_helper' 2 | 3 | module Shoulda::Matchers::Doublespeak 4 | describe DoubleImplementationRegistry do 5 | describe '.find' do 6 | it 'returns an instance of StubImplementation if given :stub' do 7 | expect(described_class.find(:stub)).to be_a(StubImplementation) 8 | end 9 | 10 | it 'returns ProxyImplementation if given :proxy' do 11 | expect(described_class.find(:proxy)).to be_a(ProxyImplementation) 12 | end 13 | 14 | it 'raises an ArgumentError if not given a registered implementation' do 15 | expect { 16 | expect(described_class.find(:something_else)) 17 | }.to raise_error(ArgumentError) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/unit/shoulda/matchers/doublespeak_spec.rb: -------------------------------------------------------------------------------- 1 | require 'doublespeak_spec_helper' 2 | 3 | module Shoulda::Matchers 4 | describe Doublespeak do 5 | describe '.double_collection_for' do 6 | it 'delegates to its world' do 7 | allow(Doublespeak.world).to receive(:double_collection_for) 8 | 9 | described_class.double_collection_for(:klass) 10 | 11 | expect(Doublespeak.world). 12 | to have_received(:double_collection_for). 13 | with(:klass) 14 | end 15 | end 16 | 17 | describe '.with_doubles_activated' do 18 | it 'delegates to its world' do 19 | allow(Doublespeak.world).to receive(:with_doubles_activated) 20 | 21 | described_class.with_doubles_activated 22 | 23 | expect(Doublespeak.world).to have_received(:with_doubles_activated) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/unit_spec_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative 'support/unit/load_environment' 2 | 3 | require 'rspec/rails' 4 | require 'rspec/matchers/fail_matchers' 5 | require 'shoulda-matchers' 6 | 7 | require 'spec_helper' 8 | 9 | $VERBOSE = true 10 | 11 | Dir[File.join(File.expand_path('support/unit/**/*.rb', __dir__))].sort.each do |file| 12 | require file 13 | end 14 | 15 | RSpec.configure do |config| 16 | config.include RSpec::Matchers::FailMatchers 17 | 18 | UnitTests::Configuration.configure_example_groups(config) 19 | 20 | config.include UnitTests::Matchers 21 | 22 | config.infer_spec_type_from_file_location! 23 | config.example_status_persistence_file_path = 'spec/examples.txt' 24 | config.alias_it_behaves_like_to(:it_supports, 'it supports') 25 | 26 | config.before(:all, type: :controller) do 27 | self.class.controller(ApplicationController) { } 28 | end 29 | 30 | config.before(:suite) do 31 | I18n.backend.send(:init_translations) 32 | end 33 | end 34 | 35 | if Rails::VERSION::STRING >= '7.2' 36 | Rails.application.deprecators.behavior = :stderr 37 | else 38 | ActiveSupport::Deprecation.behavior = :stderr 39 | end 40 | 41 | Shoulda::Matchers.configure do |config| 42 | config.integrate do |with| 43 | with.test_framework :rspec 44 | with.library :rails 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /zeus.json: -------------------------------------------------------------------------------- 1 | { 2 | "command": "ruby -r rubygems -r ./custom_plan -e Zeus.go", 3 | 4 | "plan": { 5 | "boot": { 6 | "test_environment": { 7 | "rspec": [] 8 | } 9 | } 10 | } 11 | } 12 | --------------------------------------------------------------------------------