Validating ActiveRecord Backlinks exist

Whenever a new association is added usually we also need the opposite association to ensure things get cleaned up properly during deletion.
To never forget this and audit the current state, these two tests can help.

  def all_models
    models = Dir["app/models/**/*.rb"].grep_v(/\/concerns\//)
    models.size.must_be :>, 20
    models.each { |f| require f }
    ActiveRecord::Base.descendants
  end

  it "explicity defines what should happen to dependencies" do
    bad = all_models.flat_map do |model|
      model.reflect_on_all_associations.map do |association|
        next if association.is_a?(ActiveRecord::Reflection::BelongsToReflection)
        next if association.options.key?(:through)
        next if association.options.key?(:dependent)
        "#{model.name} #{association.name}"
      end
    end.compact
    assert(
      bad.empty?,
      "These associations need a :dependent defined (most likely :destroy or nil)\n#{bad.join("\n")}"
    )
  end

  it "links all dependencies both ways so dependencies get deleted reliably" do
    bad = all_models.flat_map do |model|
      model.reflect_on_all_associations.map do |association|
        next if association.name == :audits
        next if association.options.fetch(:inverse_of, false).nil? # disabled on purpose
        next if association.inverse_of
        "#{model.name} #{association.name}"
      end
    end.compact
    assert(
      bad.empty?,
      <<~TEXT
        These associations need an inverse association.
        For example project has stages and stage has project.
        If automatic connection does not work, use `:inverse_of` option on the association.
        If inverse association is missing AND the inverse should not destroyed when dependency is destroyed, use `inverse_of: nil`.
        #{bad.join("\n")}
      TEXT
    )
  end

Leave a comment