lib/shoulda/matchers/active_record/encrypt_matcher.rb
module Shoulda module Matchers module ActiveRecord # The `encrypt` matcher tests usage of the # `encrypts` macro (Rails 7+ only). # # class Survey < ActiveRecord::Base # encrypts :access_code # end # # # RSpec # RSpec.describe Survey, type: :model do # it { should encrypt(:access_code) } # end # # # Minitest (Shoulda) # class SurveyTest < ActiveSupport::TestCase # should encrypt(:access_code) # end # # #### Qualifiers # # ##### deterministic # # class Survey < ActiveRecord::Base # encrypts :access_code, deterministic: true # end # # # RSpec # RSpec.describe Survey, type: :model do # it { should encrypt(:access_code).deterministic(true) } # end # # # Minitest (Shoulda) # class SurveyTest < ActiveSupport::TestCase # should encrypt(:access_code).deterministic(true) # end # # ##### downcase # # class Survey < ActiveRecord::Base # encrypts :access_code, downcase: true # end # # # RSpec # RSpec.describe Survey, type: :model do # it { should encrypt(:access_code).downcase(true) } # end # # # Minitest (Shoulda) # class SurveyTest < ActiveSupport::TestCase # should encrypt(:access_code).downcase(true) # end # # ##### ignore_case # # class Survey < ActiveRecord::Base # encrypts :access_code, deterministic: true, ignore_case: true # end # # # RSpec # RSpec.describe Survey, type: :model do # it { should encrypt(:access_code).ignore_case(true) } # end # # # Minitest (Shoulda) # class SurveyTest < ActiveSupport::TestCase # should encrypt(:access_code).ignore_case(true) # end # # @return [EncryptMatcher] # def encrypt(value) EncryptMatcher.new(value) end # @private class EncryptMatcher def initialize(attribute) @attribute = attribute.to_sym @options = {} end attr_reader :failure_message, :failure_message_when_negated def deterministic(deterministic) with_option(:deterministic, deterministic) end def downcase(downcase) with_option(:downcase, downcase) end def ignore_case(ignore_case) with_option(:ignore_case, ignore_case) end def matches?(subject) @subject = subject result = encrypted_attributes_included? && options_correct?( :deterministic, :downcase, :ignore_case, ) if result @failure_message_when_negated = "Did not expect to #{description} of #{class_name}" if @options.present? @failure_message_when_negated += " using " @failure_message_when_negated += @options.map { |opt, expected| ":#{opt} option as ‹#{expected}›" }.join(' and ') end @failure_message_when_negated += ", but it did" end result end def description "encrypt :#{@attribute}" end private def encrypted_attributes_included? if encrypted_attributes.include?(@attribute) true else @failure_message = "Expected to #{description} of #{class_name}, but it did not" false end end def with_option(option_name, value) @options[option_name] = value self end def options_correct?(*opts) opts.all? do |opt| next true unless @options.key?(opt) expected = @options[opt] actual = encrypted_attribute_scheme.send("#{opt}?") next true if expected == actual @failure_message = "Expected to #{description} of #{class_name} using :#{opt} option as ‹#{expected}›, but got ‹#{actual}›" false end end def encrypted_attributes @_encrypted_attributes ||= @subject.class.encrypted_attributes || [] end def encrypted_attribute_scheme @subject.class.type_for_attribute(@attribute).scheme end def class_name @subject.class.name end end end end end