thoughtbot / factory_bot

A library for setting up Ruby objects as test data.
https://thoughtbot.com
MIT License
7.92k stars 2.61k forks source link

STI type column validation #1478

Closed ngouy closed 3 years ago

ngouy commented 3 years ago

Description

When creating STI ish objects, factory bot isn't checking for the type. It doesn't raise any error if the given type doesn't exists in STI subclasses

Reproduction Steps

I have an STI pattern

# models
class ParentClass < ApplicationRecord; end

class Foo < ParentClass; end
class Bar < ParentClass; end

Thus I only have one table in db with the type colomn

Here is my factory

FactoryBot.define do
  factory :parent_class do
    type { ["Foo", "Bar"].sample }

    factory :foo, parent: :parent_class, class: "Foo"
    factory :bar, parent: :parent_class, class: "Bar"
  end
end

Now lets test some things :

ParentClass.new      # <Foo id: nil, type: nil>
Foo.new              # <Foo id: nil, type: "Foo">
build(:parent_class) # <ParentClass id: nil, type: "Bar"> // could be "Foo" also
build(:foo)          # <Foo id: nil, type: "Foo">

So far so good. But here is the bug :

ParentClass.create!(type: "UnknownSubClass") # Raises error
# ActiveRecord::SubclassNotFound Exception: The single-table inheritance mechanism failed to locate the subclass: 'UnknownSubClass'
create(:parent_class, type: "UnknownSubClass") # <ParentClass id: 1, type: "UnknownSubClass">

There is an inconsistency between the real activerecord interface and what factorybot is returning us

Expected behavior

We should get an error

Actual behavior

Everything goes smoothly 😱

System configuration

factory_bot version: 5.1.1 (updated to 6.1.0, same issue) rails version: 6.1.1 ruby version: 2.7.2

aledustet commented 3 years ago

Hello, @ngouy we have tried reproducing this scenario with the reproduction script and substituted the create! call with the process that factory_bot is using internally:

# under the hood create does something similar to this
new_class = ParentClass.new
new_class.type = "UnknownSubClass"
new_class.save!
Full reproduction script ``` ruby require "bundler/inline" gemfile(true) do source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem "factory_bot", "~> 5.0" gem "activerecord" gem "sqlite3" end require "active_record" require "factory_bot" require "minitest/autorun" require "logger" ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Schema.define do create_table :parent_classes, force: true do |t| t.string :type end end class ParentClass < ActiveRecord::Base; end class Foo < ParentClass; end class Bar < ParentClass; end FactoryBot.define do factory :parent_class do type { ["Foo", "Bar"].sample } factory :foo, parent: :parent_class, class: "Foo" factory :bar, parent: :parent_class, class: "Bar" end end # ParentClass.create(type: "UnknownSubClass") new_class = ParentClass.new new_class.type = "UnknownSubClass" new_class.save! FactoryBot.create(:parent_class, type: "UnknownSubClass") ```

The behavior is the same as you correctly described. But since this is reproducible without any factory_bot code we are going to close it. Thank you for bringing it up!