mirror of
https://github.com/thoughtbot/shoulda-matchers.git
synced 2022-11-09 12:01:38 -05:00
Add have_attached matcher for ActiveStorage (#1102)
This commit is contained in:
parent
a11361125f
commit
e4a268b5b4
5 changed files with 428 additions and 0 deletions
|
@ -23,6 +23,7 @@ require "shoulda/matchers/active_record/accept_nested_attributes_for_matcher"
|
||||||
require "shoulda/matchers/active_record/define_enum_for_matcher"
|
require "shoulda/matchers/active_record/define_enum_for_matcher"
|
||||||
require "shoulda/matchers/active_record/uniqueness"
|
require "shoulda/matchers/active_record/uniqueness"
|
||||||
require "shoulda/matchers/active_record/validate_uniqueness_of_matcher"
|
require "shoulda/matchers/active_record/validate_uniqueness_of_matcher"
|
||||||
|
require "shoulda/matchers/active_record/have_attached_matcher"
|
||||||
|
|
||||||
module Shoulda
|
module Shoulda
|
||||||
module Matchers
|
module Matchers
|
||||||
|
|
147
lib/shoulda/matchers/active_record/have_attached_matcher.rb
Normal file
147
lib/shoulda/matchers/active_record/have_attached_matcher.rb
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
module Shoulda
|
||||||
|
module Matchers
|
||||||
|
module ActiveRecord
|
||||||
|
def have_one_attached(name)
|
||||||
|
HaveAttachedMatcher.new(:one, name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def have_many_attached(name)
|
||||||
|
HaveAttachedMatcher.new(:many, name)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @private
|
||||||
|
class HaveAttachedMatcher
|
||||||
|
attr_reader :name
|
||||||
|
|
||||||
|
def initialize(macro, name)
|
||||||
|
@macro = macro
|
||||||
|
@name = name
|
||||||
|
end
|
||||||
|
|
||||||
|
def description
|
||||||
|
"have a has_#{macro}_attached called #{name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def failure_message
|
||||||
|
<<-MESSAGE
|
||||||
|
Expected #{expectation}, but this could not be proved.
|
||||||
|
#{@failure}
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
|
||||||
|
def failure_message_when_negated
|
||||||
|
<<-MESSAGE
|
||||||
|
Did not expect #{expectation}, but it does.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
|
||||||
|
def expectation
|
||||||
|
"#{model_class.name} to #{description}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def matches?(subject)
|
||||||
|
@subject = subject
|
||||||
|
reader_attribute_exists? &&
|
||||||
|
writer_attribute_exists? &&
|
||||||
|
attachments_association_exists? &&
|
||||||
|
blobs_association_exists? &&
|
||||||
|
eager_loading_scope_exists?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :subject, :macro
|
||||||
|
|
||||||
|
def reader_attribute_exists?
|
||||||
|
if subject.respond_to?(name)
|
||||||
|
true
|
||||||
|
else
|
||||||
|
@failure = "#{model_class.name} does not have a :#{name} method."
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def writer_attribute_exists?
|
||||||
|
if subject.respond_to?("#{name}=")
|
||||||
|
true
|
||||||
|
else
|
||||||
|
@failure = "#{model_class.name} does not have a :#{name}= method."
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def attachments_association_exists?
|
||||||
|
if attachments_association_matcher.matches?(subject)
|
||||||
|
true
|
||||||
|
else
|
||||||
|
@failure = attachments_association_matcher.failure_message
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def attachments_association_matcher
|
||||||
|
@_attachments_association_matcher ||=
|
||||||
|
AssociationMatcher.new(
|
||||||
|
:"has_#{macro}",
|
||||||
|
attachments_association_name,
|
||||||
|
).
|
||||||
|
conditions(name: name).
|
||||||
|
class_name('ActiveStorage::Attachment').
|
||||||
|
inverse_of(:record)
|
||||||
|
end
|
||||||
|
|
||||||
|
def attachments_association_name
|
||||||
|
case macro
|
||||||
|
when :one then
|
||||||
|
"#{name}_attachment"
|
||||||
|
when :many then
|
||||||
|
"#{name}_attachments"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def blobs_association_exists?
|
||||||
|
if blobs_association_matcher.matches?(subject)
|
||||||
|
true
|
||||||
|
else
|
||||||
|
@failure = blobs_association_matcher.failure_message
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def blobs_association_matcher
|
||||||
|
@_blobs_association_matcher ||=
|
||||||
|
AssociationMatcher.new(
|
||||||
|
:"has_#{macro}",
|
||||||
|
blobs_association_name,
|
||||||
|
).
|
||||||
|
through(attachments_association_name).
|
||||||
|
class_name('ActiveStorage::Blob').
|
||||||
|
source(:blob)
|
||||||
|
end
|
||||||
|
|
||||||
|
def blobs_association_name
|
||||||
|
case macro
|
||||||
|
when :one then
|
||||||
|
"#{name}_blob"
|
||||||
|
when :many then
|
||||||
|
"#{name}_blobs"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def eager_loading_scope_exists?
|
||||||
|
if model_class.respond_to?("with_attached_#{name}")
|
||||||
|
true
|
||||||
|
else
|
||||||
|
@failure = "#{model_class.name} does not have a " \
|
||||||
|
":with_attached_#{name} scope."
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def model_class
|
||||||
|
subject.class
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -47,6 +47,10 @@ module UnitTests
|
||||||
active_record_version >= 5
|
active_record_version >= 5
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def active_record_supports_active_storage?
|
||||||
|
active_record_version >= 5.2
|
||||||
|
end
|
||||||
|
|
||||||
def active_record_supports_validate_presence_on_active_storage?
|
def active_record_supports_validate_presence_on_active_storage?
|
||||||
active_record_version >= '6.0.0.beta1'
|
active_record_version >= '6.0.0.beta1'
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,5 +18,9 @@ module UnitTests
|
||||||
def rails_5_x?
|
def rails_5_x?
|
||||||
rails_version =~ '~> 5.0'
|
rails_version =~ '~> 5.0'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def rails_gte_5_2?
|
||||||
|
rails_version >= 5.2
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,272 @@
|
||||||
|
require 'unit_spec_helper'
|
||||||
|
|
||||||
|
describe Shoulda::Matchers::ActiveRecord::HaveAttachedMatcher, type: :model do
|
||||||
|
if active_record_supports_active_storage?
|
||||||
|
before do
|
||||||
|
create_table :active_storage_blobs do |t|
|
||||||
|
t.string :key, null: false
|
||||||
|
t.string :filename, null: false
|
||||||
|
t.string :content_type
|
||||||
|
t.text :metadata
|
||||||
|
t.bigint :byte_size, null: false
|
||||||
|
t.string :checksum, null: false
|
||||||
|
t.datetime :created_at, null: false
|
||||||
|
|
||||||
|
t.index [:key], unique: true
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :active_storage_attachments do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.references :record, null: false, polymorphic: true, index: false
|
||||||
|
t.references :blob, null: false
|
||||||
|
|
||||||
|
t.datetime :created_at, null: false
|
||||||
|
|
||||||
|
t.index [:record_type, :record_id, :name, :blob_id],
|
||||||
|
name: 'index_active_storage_attachments_uniqueness', unique: true
|
||||||
|
|
||||||
|
# The original rails migration has a foreign key.
|
||||||
|
# Since this messes up the clearing of the database, it's removed here.
|
||||||
|
# t.foreign_key :active_storage_blobs, column: :blob_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'have_one_attached' do
|
||||||
|
describe '#description' do
|
||||||
|
it 'returns the message with the name of the association' do
|
||||||
|
matcher = have_one_attached(:avatar)
|
||||||
|
expect(matcher.description).
|
||||||
|
to eq('have a has_one_attached called avatar')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the attached exists on the model' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_having_one_attached(:avatar)
|
||||||
|
expect { have_one_attached(:avatar) }.
|
||||||
|
to match_against(record).
|
||||||
|
or_fail_with(<<-MESSAGE)
|
||||||
|
Did not expect User to have a has_one_attached called avatar, but it does.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the reader attribute does not exist' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_having_one_attached(:avatar, remove_reader: true)
|
||||||
|
expect { have_one_attached(:avatar) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected User to have a has_one_attached called avatar, but this could not be proved.
|
||||||
|
User does not have a :avatar method.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the writer attribute does not exist' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_having_one_attached(:avatar, remove_writer: true)
|
||||||
|
expect { have_one_attached(:avatar) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected User to have a has_one_attached called avatar, but this could not be proved.
|
||||||
|
User does not have a :avatar= method.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the attachments association does not exist' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_having_one_attached(:avatar, remove_attachments: true)
|
||||||
|
expect { have_one_attached(:avatar) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected User to have a has_one_attached called avatar, but this could not be proved.
|
||||||
|
Expected User to have a has_one association called avatar_attachment (no association called avatar_attachment)
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the blobs association is invalid' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_having_one_attached(:avatar, invalidate_blobs: true)
|
||||||
|
expect { have_one_attached(:avatar) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected User to have a has_one_attached called avatar, but this could not be proved.
|
||||||
|
Expected User to have a has_one association called avatar_blob through avatar_attachment (avatar_blob should resolve to ActiveStorage::Blob for class_name)
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the eager loading scope does not exist' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_having_one_attached(:avatar, remove_eager_loading_scope: true)
|
||||||
|
expect { have_one_attached(:avatar) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with <<-MESSAGE
|
||||||
|
Expected User to have a has_one_attached called avatar, but this could not be proved.
|
||||||
|
User does not have a :with_attached_avatar scope.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'have_many_attached' do
|
||||||
|
describe '#description' do
|
||||||
|
it 'returns the message with the name of the association' do
|
||||||
|
matcher = have_many_attached(:avatars)
|
||||||
|
expect(matcher.description).
|
||||||
|
to eq('have a has_many_attached called avatars')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the attached exists on the model' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_having_many_attached(:avatars)
|
||||||
|
expect { have_many_attached(:avatars) }.
|
||||||
|
to match_against(record).
|
||||||
|
or_fail_with(<<-MESSAGE)
|
||||||
|
Did not expect User to have a has_many_attached called avatars, but it does.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the reader attribute does not exist' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_having_many_attached(:avatars, remove_reader: true)
|
||||||
|
expect { have_many_attached(:avatars) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected User to have a has_many_attached called avatars, but this could not be proved.
|
||||||
|
User does not have a :avatars method.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the writer attribute does not exist' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_having_many_attached(:avatars, remove_writer: true)
|
||||||
|
expect { have_many_attached(:avatars) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected User to have a has_many_attached called avatars, but this could not be proved.
|
||||||
|
User does not have a :avatars= method.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the attachments association does not exist' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_having_many_attached(:avatars, remove_attachments: true)
|
||||||
|
expect { have_many_attached(:avatars) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected User to have a has_many_attached called avatars, but this could not be proved.
|
||||||
|
Expected User to have a has_many association called avatars_attachments (no association called avatars_attachments)
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the blobs association is invalid' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_having_many_attached(:avatars, invalidate_blobs: true)
|
||||||
|
expect { have_many_attached(:avatars) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected User to have a has_many_attached called avatars, but this could not be proved.
|
||||||
|
Expected User to have a has_many association called avatars_blobs through avatars_attachments (avatars_blobs should resolve to ActiveStorage::Blob for class_name)
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and the eager loading scope does not exist' do
|
||||||
|
it 'matches' do
|
||||||
|
record = record_having_many_attached(:avatars, remove_eager_loading_scope: true)
|
||||||
|
expect { have_many_attached(:avatars) }.
|
||||||
|
not_to match_against(record).
|
||||||
|
and_fail_with(<<-MESSAGE)
|
||||||
|
Expected User to have a has_many_attached called avatars, but this could not be proved.
|
||||||
|
User does not have a :with_attached_avatars scope.
|
||||||
|
MESSAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_having_one_attached(
|
||||||
|
attached_name,
|
||||||
|
model_name: 'User',
|
||||||
|
remove_reader: false,
|
||||||
|
remove_writer: false,
|
||||||
|
remove_attachments: false,
|
||||||
|
invalidate_blobs: false,
|
||||||
|
remove_eager_loading_scope: false
|
||||||
|
)
|
||||||
|
model = define_model(model_name) do
|
||||||
|
has_one_attached attached_name
|
||||||
|
|
||||||
|
if remove_reader
|
||||||
|
undef_method attached_name
|
||||||
|
end
|
||||||
|
|
||||||
|
if remove_writer
|
||||||
|
undef_method "#{attached_name}="
|
||||||
|
end
|
||||||
|
|
||||||
|
if remove_attachments
|
||||||
|
reflections.delete("#{attached_name}_attachment")
|
||||||
|
end
|
||||||
|
|
||||||
|
if invalidate_blobs
|
||||||
|
reflections["#{attached_name}_blob"].options[:class_name] = 'User'
|
||||||
|
end
|
||||||
|
|
||||||
|
if remove_eager_loading_scope
|
||||||
|
instance_eval <<-CODE, __FILE__, __LINE__ + 1
|
||||||
|
undef with_attached_#{attached_name}
|
||||||
|
CODE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
model.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_having_many_attached(
|
||||||
|
attached_name,
|
||||||
|
model_name: 'User',
|
||||||
|
remove_reader: false,
|
||||||
|
remove_writer: false,
|
||||||
|
remove_attachments: false,
|
||||||
|
invalidate_blobs: false,
|
||||||
|
remove_eager_loading_scope: false
|
||||||
|
)
|
||||||
|
model = define_model(model_name) do
|
||||||
|
has_many_attached attached_name
|
||||||
|
|
||||||
|
if remove_reader
|
||||||
|
undef_method attached_name
|
||||||
|
end
|
||||||
|
|
||||||
|
if remove_writer
|
||||||
|
undef_method "#{attached_name}="
|
||||||
|
end
|
||||||
|
|
||||||
|
if remove_attachments
|
||||||
|
reflections.delete("#{attached_name}_attachments")
|
||||||
|
end
|
||||||
|
|
||||||
|
if invalidate_blobs
|
||||||
|
reflections["#{attached_name}_blobs"].options[:class_name] = 'User'
|
||||||
|
end
|
||||||
|
|
||||||
|
if remove_eager_loading_scope
|
||||||
|
instance_eval <<-CODE, __FILE__, __LINE__ + 1
|
||||||
|
undef with_attached_#{attached_name}
|
||||||
|
CODE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
model.new
|
||||||
|
end
|
Loading…
Add table
Reference in a new issue