Trouble in RSpec test - saving parent record twice

456 views Asked by At

I'm new at Rails programming, and my RSpec test is failing when trying to save nested records. The logs from the test suggest that ActiveRecord is trying to save the parent twice, which results in a silent database save failure.

sign_up_spec.rb (sorry for the omission!)

require 'rails_helper'

RSpec.feature "Accounts", type: :feature do
  let(:user) { FactoryGirl.create(:user) }

  scenario "creating an account" do
    visit root_path
    click_link "Sign Up"
    fill_in "Account name", :with => "Test Firm"
    fill_in "Username", :with => user.username
    fill_in "Password", :with => user.password
    fill_in "Password confirmation", :with => user.password_confirmation
    click_button "Create Account"
    success_message = "Your account has been successfully created."
    expect(page).to have_content(success_message)
    expect(page).to have_content("Signed in as #{user.username}")
  end
end

account.rb

class Account < ActiveRecord::Base
  belongs_to :owner, :class_name => "User"
  accepts_nested_attributes_for :owner
end

user.rb

class User < ActiveRecord::Base
  has_secure_password
  validates :username, presence: true, uniqueness: { case_sensitive: false }
  validates :password, presence: true, length: { minimum: 8, maximum: 20 }
end

account_controller.rb

class AccountsController < ApplicationController
  def new
    @account = Account.new
    @account.build_owner
  end

  def create
    account = Account.create(account_params)
    env["warden"].set_user(account.owner, :scope => :user)
    env["warden"].set_user(account, :scope => :account)
    flash[:success] = "Your account has been successfully created."
    redirect_to root_url
  end

  private

    def account_params
     params.require(:account).permit(:account_name, {:owner_attributes => [
        :username, :password, :password_confirmation
        ]})
    end
end

account new.html.erb

<h2>Sign Up</h2>
<%= form_for(@account) do |account| %>
  <p>
    <%= account.label :account_name %><br>
    <%= account.text_field :account_name %>
  </p>
  <%= account.fields_for :owner do |owner| %>
    <p>
      <%= owner.label :username %><br>
      <%= owner.text_field :username %>
    </p>
    <p>
      <%= owner.label :password %><br>
      <%= owner.password_field :password %>
    </p>
    <p>
      <%= owner.label :password_confirmation %><br>
      <%= owner.password_field :password_confirmation %>
    </p>
  <% end %>
  <%= account.submit %>
<% end %>

The following excerpt from the test result log below suggests that ActiveRecord is trying to insert the User record twice, once during the Account#new function, and again in the Account#create function. I've added comments with my understanding of what's going on through the process:

test log

Started GET "/sign_up" for 127.0.0.1 at 2015-06-17 15:50:43 +0000
Processing by AccountsController#new as HTML
Rendered accounts/new.html.erb within layouts/application (45.6ms)
Completed 200 OK in 110ms (Views: 51.6ms | ActiveRecord: 6.9ms)

# it appears that this action is happening from the Account#new method;
# why would @account.build_owner cause a database action?
(0.4ms)  SAVEPOINT active_record_1
User Exists (30.8ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."username") = LOWER('user1') LIMIT 1
SQL (3.8ms)  INSERT INTO "users" ("username", "password_digest", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["username", "user1"], ["password_digest", "blahblah"], ["created_at", "2015-06-17 13:15:40.667965"], ["updated_at", "2015-06-17 13:15:40.667965"]]
(1.4ms)  RELEASE SAVEPOINT active_record_1
Started POST "/accounts" for 127.0.0.1 at 2015-06-17 13:15:40 +0000
Processing by AccountsController#create as HTML
Parameters: {"utf8"=>"✓", "account"=>{"account_name"=>"Test Firm", "owner_attributes"=>{"username"=>"user1", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}}, "commit"=>"Create Account"}

# the Account#create function wants to add the user a second time (or more
# accurately, the first time), but the user parent already exists from above
# so it fails the uniqueness test
(0.2ms)  SAVEPOINT active_record_1
User Exists (0.9ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."username") = LOWER('user1') LIMIT 1

# which causes the query to bomb
(0.2ms)  ROLLBACK TO SAVEPOINT active_record_1

#and neither the parent or child records are created
#<Account id: nil, account_name: nil, created_at: nil, updated_at: nil, owner_id: nil>
#<User id: nil, username: "user1", created_at: nil, updated_at: nil, password_digest: "blahblah">
Redirected to http://www.example.com/
Completed 302 Found in 17ms (ActiveRecord: 1.3ms)
Started GET "/" for 127.0.0.1 at 2015-06-17 13:15:40 +0000
Processing by StaticPagesController#home as HTML
Rendered static_pages/home.html.erb within layouts/application (0.5ms)
Completed 200 OK in 6ms (Views: 5.3ms | ActiveRecord: 0.0ms)
(4.3ms)  ROLLBACK

Can anyone help me understand what I'm missing? Thanks

1

There are 1 answers

0
Rob Wise On BEST ANSWER

The issue is in sign_up_spec.rb. Your test has a let for user, which means the first time you mention user in your tests it will create a user. However, your application code is supposed to create the user itself.

As I said in the comment, this is why in your test log you see that it successfully completes the get request and then, before the post request, the user gets created. When your code tries to create a user after submitting the form, it's already been created!

I commented your test spec to make this more clear:

let(:user) { FactoryGirl.create(:user) }
scenario "creating an account" do
    visit root_path
    click_link "Sign Up" # this is the successful get request
    fill_in "Account name", :with => "Test Firm"
    fill_in "Username", :with => user.username # this triggers the let and creates a user
    fill_in "Password", :with => user.password
    fill_in "Password confirmation", :with => user.password_confirmation
    click_button "Create Account" # this starts the post request
    success_message = "Your account has been successfully created."
    expect(page).to have_content(success_message)
    expect(page).to have_content("Signed in as #{user.username}")
end

To fix this, just change the let statement to use FactoryGirl's attributes_for method instead and use with: user[:username] instead of with: user.username.

Also, you're passing the symbol version of owner in the nested form, which means Rails is going to build a new instance of owner and then assign it to the @account. You already, however, built an instance of owner for @account inside the new action. So that code is redundant. Either remove the build_owner line from the controller action, or pass @account.owner to the fields_for call in your view.