From f82329a67957d84fe0e40154666d6b53a9025715 Mon Sep 17 00:00:00 2001 From: Yuriy Orlov Date: Thu, 12 Sep 2019 17:05:13 +0300 Subject: [PATCH] Add have_implicit_order_column matcher Add a matcher that can test the new [implicit_order_column][1] class property that is available on ActiveRecord classes in Rails 6. [1]: https://github.com/rails/rails/pull/34480 Co-authored-by: Elliot Winkler --- lib/shoulda/matchers/active_record.rb | 1 + .../have_implicit_order_column.rb | 106 +++++++++++ lib/shoulda/matchers/rails_shim.rb | 4 + .../unit/active_record/create_table.rb | 75 +++++--- .../unit/helpers/active_record_versions.rb | 4 + spec/support/unit/helpers/model_builder.rb | 4 + spec/support/unit/rails_application.rb | 3 +- .../have_implicit_order_column_spec.rb | 173 ++++++++++++++++++ 8 files changed, 338 insertions(+), 32 deletions(-) create mode 100644 lib/shoulda/matchers/active_record/have_implicit_order_column.rb create mode 100644 spec/unit/shoulda/matchers/active_record/have_implicit_order_column_spec.rb diff --git a/lib/shoulda/matchers/active_record.rb b/lib/shoulda/matchers/active_record.rb index fd1ff777..c0bdd2ed 100644 --- a/lib/shoulda/matchers/active_record.rb +++ b/lib/shoulda/matchers/active_record.rb @@ -14,6 +14,7 @@ require "shoulda/matchers/active_record/association_matchers/model_reflection" require "shoulda/matchers/active_record/association_matchers/option_verifier" require "shoulda/matchers/active_record/have_db_column_matcher" require "shoulda/matchers/active_record/have_db_index_matcher" +require "shoulda/matchers/active_record/have_implicit_order_column" require "shoulda/matchers/active_record/have_readonly_attribute_matcher" require "shoulda/matchers/active_record/have_rich_text_matcher" require "shoulda/matchers/active_record/have_secure_token_matcher" diff --git a/lib/shoulda/matchers/active_record/have_implicit_order_column.rb b/lib/shoulda/matchers/active_record/have_implicit_order_column.rb new file mode 100644 index 00000000..4008b992 --- /dev/null +++ b/lib/shoulda/matchers/active_record/have_implicit_order_column.rb @@ -0,0 +1,106 @@ +module Shoulda + module Matchers + module ActiveRecord + # The `have_implicit_order_column` matcher tests that the model has `implicit_order_column` + # assigned to one of the table columns. (Rails 6+ only) + # + # class Product < ApplicationRecord + # self.implicit_order_column = :created_at + # end + # + # # RSpec + # RSpec.describe Product, type: :model do + # it { should have_implicit_order_column(:created_at) } + # end + # + # # Minitest (Shoulda) + # class ProductTest < ActiveSupport::TestCase + # should have_implicit_order_column(:created_at) + # end + # + # @return [HaveImplicitOrderColumnMatcher] + # + if RailsShim.active_record_gte_6? + def have_implicit_order_column(column_name) + HaveImplicitOrderColumnMatcher.new(column_name) + end + end + + # @private + class HaveImplicitOrderColumnMatcher + attr_reader :failure_message + + def initialize(column_name) + @column_name = column_name + end + + def matches?(subject) + @subject = subject + check_column_exists! + check_implicit_order_column_matches! + true + rescue SecondaryCheckFailedError => error + @failure_message = Shoulda::Matchers.word_wrap( + "Expected #{model.name} to #{expectation}, " + + "but that could not be proved: #{error.message}." + ) + false + rescue PrimaryCheckFailedError => error + @failure_message = Shoulda::Matchers.word_wrap( + "Expected #{model.name} to #{expectation}, but #{error.message}." + ) + false + end + + def failure_message_when_negated + Shoulda::Matchers.word_wrap( + "Expected #{model.name} not to #{expectation}, but it did." + ) + end + + def description + expectation + end + + private + + attr_reader :column_name, :subject + + def check_column_exists! + matcher = HaveDbColumnMatcher.new(column_name) + + if !matcher.matches?(@subject) + raise SecondaryCheckFailedError.new( + "The :#{model.table_name} table does not have a " + + ":#{column_name} column" + ) + end + end + + def check_implicit_order_column_matches! + if model.implicit_order_column.to_s != column_name.to_s + message = + if model.implicit_order_column.nil? + "implicit_order_column is not set" + else + "it is :#{model.implicit_order_column}" + end + + raise PrimaryCheckFailedError.new(message) + end + end + + def model + subject.class + end + + def expectation + "have an implicit_order_column of :#{column_name}" + end + + class SecondaryCheckFailedError < StandardError; end + class PrimaryCheckFailedError < StandardError; end + end + end + end +end diff --git a/lib/shoulda/matchers/rails_shim.rb b/lib/shoulda/matchers/rails_shim.rb index 06676b4c..ae36345c 100644 --- a/lib/shoulda/matchers/rails_shim.rb +++ b/lib/shoulda/matchers/rails_shim.rb @@ -21,6 +21,10 @@ module Shoulda Gem::Requirement.new('>= 5').satisfied_by?(active_record_version) end + def active_record_gte_6? + Gem::Requirement.new('>= 6').satisfied_by?(active_record_version) + end + def active_record_version Gem::Version.new(::ActiveRecord::VERSION::STRING) rescue NameError diff --git a/spec/support/unit/active_record/create_table.rb b/spec/support/unit/active_record/create_table.rb index 25dde568..9fea462c 100644 --- a/spec/support/unit/active_record/create_table.rb +++ b/spec/support/unit/active_record/create_table.rb @@ -25,7 +25,7 @@ module UnitTests &block ) @table_name = table_name - @columns = columns + @columns = normalize_columns(columns) @connection = connection @customizer = block || proc {} end @@ -66,6 +66,26 @@ module UnitTests to: UnitTests::DatabaseHelpers, ) + def normalize_columns(columns) + if columns.is_a?(Hash) + if columns.values.first.is_a?(Hash) + columns + else + columns.transform_values do |value| + if value == false + value + else + { type: value } + end + end + end + else + columns.inject({}) do |hash, column_name| + hash.merge(column_name => { type: :string }) + end + end + end + def add_columns_to_table(table) columns.each do |column_name, column_specification| add_column_to_table(table, column_name, column_specification) @@ -75,39 +95,34 @@ module UnitTests end def add_column_to_table(table, column_name, column_specification) - if column_specification.is_a?(Hash) - column_specification = column_specification.dup - column_type = column_specification.delete(:type) - column_options = column_specification.delete(:options) { {} } + column_specification = column_specification.dup + column_type = column_specification.delete(:type) + column_options = column_specification.delete(:options) { {} } - if column_options[:array] - if !active_record_supports_array_columns? - raise ArgumentError.new( - 'An array column is being added to a table, but this version ' + - "of ActiveRecord (#{active_record_version}) " + - 'does not support array columns.', - ) - end - - if !database_supports_array_columns? - raise ArgumentError.new( - 'An array column is being added to a table, but this ' + - "database adapter (#{database_adapter}) " + - 'does not support array columns.', - ) - end - end - - if column_specification.any? + if column_options[:array] + if !active_record_supports_array_columns? raise ArgumentError.new( - "Invalid column specification.\nYou need to put " + - "#{column_specification.keys.map(&:inspect).to_sentence} " + - 'inside an :options key!', + 'An array column is being added to a table, but this version ' + + "of ActiveRecord (#{active_record_version}) " + + 'does not support array columns.', ) end - else - column_type = column_specification - column_options = {} + + if !database_supports_array_columns? + raise ArgumentError.new( + 'An array column is being added to a table, but this ' + + "database adapter (#{database_adapter}) " + + 'does not support array columns.', + ) + end + end + + if column_specification.any? + raise ArgumentError.new( + "Invalid column specification.\nYou need to put " + + "#{column_specification.keys.map(&:inspect).to_sentence} " + + 'inside an :options key!', + ) end table.column(column_name, column_type, column_options) diff --git a/spec/support/unit/helpers/active_record_versions.rb b/spec/support/unit/helpers/active_record_versions.rb index 3cbde9cf..cb3f35ac 100644 --- a/spec/support/unit/helpers/active_record_versions.rb +++ b/spec/support/unit/helpers/active_record_versions.rb @@ -50,5 +50,9 @@ module UnitTests def active_record_supports_validate_presence_on_active_storage? active_record_version >= '6.0.0.beta1' end + + def active_record_supports_implicit_order_column? + active_record_version >= '6.0.0.beta1' + end end end diff --git a/spec/support/unit/helpers/model_builder.rb b/spec/support/unit/helpers/model_builder.rb index 87c2a1ad..39393161 100644 --- a/spec/support/unit/helpers/model_builder.rb +++ b/spec/support/unit/helpers/model_builder.rb @@ -10,6 +10,10 @@ module UnitTests ModelBuilder.define_model(*args, &block) end + def define_model_instance(*args, &block) + define_model(*args, &block).new + end + def define_model_class(*args, &block) ModelBuilder.define_model_class(*args, &block) end diff --git a/spec/support/unit/rails_application.rb b/spec/support/unit/rails_application.rb index 6ee85425..4f57ccc4 100644 --- a/spec/support/unit/rails_application.rb +++ b/spec/support/unit/rails_application.rb @@ -204,8 +204,7 @@ end bundle.remove_gem 'byebug' bundle.remove_gem 'web-console' bundle.add_gem 'pg' - bundle.remove_gem 'sqlite3' - bundle.add_gem 'sqlite3', '~> 1.3.6' + bundle.add_gem 'sqlite', '~> 1.3.6' end end diff --git a/spec/unit/shoulda/matchers/active_record/have_implicit_order_column_spec.rb b/spec/unit/shoulda/matchers/active_record/have_implicit_order_column_spec.rb new file mode 100644 index 00000000..2cdf4b43 --- /dev/null +++ b/spec/unit/shoulda/matchers/active_record/have_implicit_order_column_spec.rb @@ -0,0 +1,173 @@ +require 'unit_spec_helper' + +describe Shoulda::Matchers::ActiveRecord::HaveImplicitOrderColumnMatcher, type: :model do + if active_record_supports_implicit_order_column? + context 'when the given column exists' do + context 'when an implicit_order_column is set on the model' do + context 'and it matches the given column name' do + context 'and the column name is a symbol' do + it 'matches' do + record = record_with_implicit_order_column_on( + :created_at, + class_name: 'Employee', + columns: [:created_at] + ) + + expect { have_implicit_order_column(:created_at) } + .to match_against(record) + .or_fail_with(<<~MESSAGE, wrap: true) + Expected Employee not to have an implicit_order_column of + :created_at, but it did. + MESSAGE + end + end + + context 'and the column name is a string' do + it 'matches' do + record = record_with_implicit_order_column_on( + :created_at, + class_name: 'Employee', + columns: [:created_at] + ) + + expect { have_implicit_order_column('created_at') } + .to match_against(record) + .or_fail_with(<<~MESSAGE, wrap: true) + Expected Employee not to have an implicit_order_column of + :created_at, but it did. + MESSAGE + end + end + end + + context 'and it does not match the given column name' do + context 'and the column name is a symbol' do + it 'does not match, producing an appropriate message' do + record = record_with_implicit_order_column_on( + :created_at, + class_name: 'Employee', + columns: [:created_at, :email] + ) + + expect { have_implicit_order_column(:email) } + .not_to match_against(record) + .and_fail_with(<<-MESSAGE, wrap: true) + Expected Employee to have an implicit_order_column of :email, + but it is :created_at. + MESSAGE + end + end + + context 'and the column name is a string' do + it 'does not match, producing an appropriate message' do + record = record_with_implicit_order_column_on( + :created_at, + class_name: 'Employee', + columns: [:created_at, :email] + ) + + expect { have_implicit_order_column('email') } + .not_to match_against(record) + .and_fail_with(<<-MESSAGE, wrap: true) + Expected Employee to have an implicit_order_column of :email, + but it is :created_at. + MESSAGE + end + end + end + end + + context 'when no implicit_order_column is set on the model' do + context 'and the given column name is a symbol' do + it 'does not match, producing an appropriate message' do + record = record_without_implicit_order_column( + class_name: 'Employee', + columns: [:created_at] + ) + + expect { have_implicit_order_column(:created_at) } + .not_to match_against(record) + .and_fail_with(<<-MESSAGE, wrap: true) + Expected Employee to have an implicit_order_column of + :created_at, but implicit_order_column is not set. + MESSAGE + end + end + + context 'and the given column name is a string' do + it 'does not match, producing an appropriate message' do + record = record_without_implicit_order_column( + class_name: 'Employee', + columns: [:created_at] + ) + + expect { have_implicit_order_column('created_at') } + .not_to match_against(record) + .and_fail_with(<<-MESSAGE, wrap: true) + Expected Employee to have an implicit_order_column of + :created_at, but implicit_order_column is not set. + MESSAGE + end + end + end + end + + context 'when the given column does not exist' do + context 'and it is a symbol' do + it 'does not match, producing an appropriate message' do + record = record_without_any_columns(class_name: 'Employee') + + expect { have_implicit_order_column(:whatever) } + .not_to match_against(record) + .and_fail_with(<<-MESSAGE, wrap: true) + Expected Employee to have an implicit_order_column of :whatever, + but that could not be proved: The :employees table does not have a + :whatever column. + MESSAGE + end + end + + context 'and it is a string' do + it 'does not match, producing an appropriate message' do + record = record_without_any_columns(class_name: 'Employee') + + expect { have_implicit_order_column('whatever') } + .not_to match_against(record) + .and_fail_with(<<-MESSAGE, wrap: true) + Expected Employee to have an implicit_order_column of :whatever, + but that could not be proved: The :employees table does not have a + :whatever column. + MESSAGE + end + end + end + + describe '#description' do + it 'returns the correct description' do + matcher = have_implicit_order_column(:created_at) + + expect(matcher.description).to eq( + 'have an implicit_order_column of :created_at' + ) + end + end + + def record_with_implicit_order_column_on( + column_name, + class_name:, + columns: { column_name => :string } + ) + define_model_instance(class_name, columns) do |model| + model.implicit_order_column = column_name + end + end + + def record_without_implicit_order_column(class_name:, columns:) + define_model_instance(class_name, columns) + end + + def record_without_any_columns(class_name:) + define_model_instance(class_name) + end + end +end