Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions spec/models/acts_as_paranoid_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require "rails_helper"

RSpec.describe "acts_as_paranoid" do
let(:currently_probably_buggy_classes_ignored) do
%w[]
end
let(:allows_multiple_deleted) { "(deleted_at IS NULL)" }

it "checks that all activerecord models using acts_as_paranoid have the deleted exclusions on unique indexes" do
errors = []
found_ignored_error_indexes = []
Zeitwerk::Loader.eager_load_all
expect(ApplicationRecord.descendants.count).to be >= 54 # make sure we are actually testing all model classes
ApplicationRecord.descendants.each do |clazz|
next if clazz.abstract_class?
next unless clazz.paranoid?
unique_indexes = ApplicationRecord.connection_pool.with_connection do |connection|
connection.indexes(clazz.table_name).select(&:unique)
end
unique_indexes.each do |idx|
next if idx.columns == ["external_id"] # it is ok for external_id to be unique
if currently_probably_buggy_classes_ignored.include?(idx.name)
found_ignored_error_indexes << idx.name
next
end
unless idx.where&.include?(allows_multiple_deleted)
errors << "#{idx.name} on #{clazz} uses acts_as_paranoid but has a unique index without #{allows_multiple_deleted} but it does have: #{idx.where}"
end
end
end
expect(errors).to be_empty
expect(found_ignored_error_indexes).to match_array(currently_probably_buggy_classes_ignored)
end
end
45 changes: 45 additions & 0 deletions spec/models/soft_deleted_model_shared_example_coverage_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require "rails_helper"

RSpec.describe "soft-deleted model shared example coverage" do
let(:skip_classes) do
%w[]
end

let(:todo_currently_missing_specs) do
%w[
ContactTopicAnswer
CaseContact
]
end

it "checks that all acts_as_paranoid models have specs that include the soft-deleted model shared example" do
missing = []
Zeitwerk::Loader.eager_load_all

ApplicationRecord.descendants.each do |clazz|
next if clazz.abstract_class?
next unless clazz.paranoid?
next if skip_classes.include?(clazz.name)
next if todo_currently_missing_specs.include?(clazz.name)

source_file = Object.const_source_location(clazz.name)&.first
next unless source_file

spec_file = source_file
.sub(%r{/app/models/}, "/spec/models/")
.sub(/\.rb$/, "_spec.rb")

unless File.exist?(spec_file)
missing << "#{clazz.name}: spec file not found (expected #{spec_file})"
next
end

contents = File.read(spec_file)
unless contents.include?('"a soft-deleted model"')
missing << clazz.name.to_s
end
end

expect(missing).to be_empty, "The following paranoid models are missing the shared example:\n#{missing.join("\n")}"
end
end
4 changes: 4 additions & 0 deletions spec/support/factory_bot.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# frozen_string_literal: true

def described_class_factory
described_class.name.gsub("::", "").underscore
end

RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods

Expand Down
34 changes: 34 additions & 0 deletions spec/support/shared_examples/soft_deleted_model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
RSpec.shared_examples_for "a soft-deleted model" do |skip_ignores_deleted_records_in_validations_check: false, skip_deleted_at_index_check: false|
# for usage with acts_as_paranoid models

it { is_expected.to have_db_column(:deleted_at) }

unless skip_deleted_at_index_check
it { is_expected.to have_db_index(:deleted_at) }
end

it "cannot be found, by default" do
model ||= create(described_class_factory)
model.destroy!
expect(described_class.find_by(id: model.id)).to be_nil
end

it "returned when unscoped" do
model ||= create(described_class_factory)
model.destroy!
expect(described_class.unscoped.find_by(id: model.id)).to be_present
end

context "uniqueness" do
it "ignores deleted records in validations" do
unless skip_ignores_deleted_records_in_validations_check
obj = create(described_class_factory)
new_obj = obj.dup
expect(new_obj).not_to be_valid
obj.destroy!
expect(new_obj).to be_valid
expect { new_obj.save! }.not_to raise_exception
end
end
end
end
Loading