Search code examples
ruby-on-railsvalidationfabrication-gem

How do i fabricate a model with validate presence on has_one relation with foreign key constraint


I seem to run into a some kind of circular relationships that the two solutions in the gem's documentation won't solve for me. See the example below. Is this meant to be done differently?

One would argue that because one object could not really be persisted without the other they ought to just be one model. I think it's better to extract all the logic regarding authentication to it's seperate model in order not to bloat the user. Most of the time credential stuff is only used when creating sessions, whereas the user is used all the time.

create_table "credentials", force: :cascade do |t|
  t.bigint "user_id", null: false
  ...
  t.index ["user_id"], name: "index_credentials_on_user_id"
end

add_foreign_key "credentials", "users"
class Credential < ApplicationRecord
  belongs_to :user, inverse_of: :credential
end

class User < ApplicationRecord
  has_one :credential, inverse_of: :user
  validates :credential, presence: true
end
Fabricator(:user_base, class_name: :user)

Fabricator(:user, from: :user_base) do
  credential
end

Fabricator(:credential) do
  user(fabricator: :user_base)
end
irb(main):001:0> Fabricate(:user)
  TRANSACTION (0.1ms)  BEGIN
  TRANSACTION (0.1ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):1:in `<main>'
ActiveRecord::RecordInvalid (Validation failed: Credential can't be blank)
irb(main):002:0> Fabricate(:credential)
Traceback (most recent call last):
        2: from (irb):1:in `<main>'
        1: from (irb):2:in `rescue in <main>'
ActiveRecord::RecordInvalid (Validation failed: Credential can't be blank)
irb(main):003:0> Fabricate.build(:user).save
  TRANSACTION (0.2ms)  BEGIN
  User Create (0.8ms)  INSERT INTO "users" ("email", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["email", "[email protected]"], ["created_at", "2021-05-29 18:19:09.312429"], ["updated_at", "2021-05-29 18:19:09.312429"]]
  Credential Create (0.9ms)  INSERT INTO "credentials" ("user_id", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["user_id", 19], ["created_at", "2021-05-29 18:19:09.319411"], ["updated_at", "2021-05-29 18:19:09.319411"]]
  TRANSACTION (41.2ms)  COMMIT
=> true

Solution

  • The way you're solving this would certainly work. The way I normally recommend people solve this is by overriding the inverse relationships. ActiveRecord will do the right thing in this case.

    Fabricator(:user) do
      credential { Fabricate.build(:credential, user: nil) }
    end
    
    Fabricator(:credential) do
      user { Fabricate.build(:user, credential: nil) }
    end