Polymorphic STI Has One Bug Strikes!

Posted by dave
on Tuesday, September 29

Just ran across one of the bugs related to polymorphic single-table inheritance and has_one relationships in ActiveRecord. I’m not totally sure, but I suspect it’s related to this one.

Given a set of (simplified) classes set up like this:

def User < ActiveRecord::Base
end
def Customer < User
  has_one :delivery_address, :as => :addressable
end
def Address < ActiveRecord::Base
    belongs_to :addressable, :polymorphic => true
end
def DeliveryAddress < Address
end

Associating them worked fine:

>> customer = Customer.make

>> delivery_address = DeliveryAddress.make

>>customer.delivery_address = delivery_address

And then the freakiness started:

>> customer.valid? # => true

>> customer.delivery_address.valid? # => false

>> customer.delivery_address.addressable.valid? # => false

>> customer.delivery_address.addressable.errors.full_messages

# => ["Delivery address can't be blank"]

At this point I was thinking, in capital letters, “WTF?”. A bit of poking around in the console revealed a clue:

=> #<DeliveryAddress id: 35, firstname: "Tom", lastname: "West", address1: "7308 Mohr Roads", address2: nil, city: "Beahanport", state_id: nil, zipcode: "sectorBR", country_id: 250, phone: "1-726-092-6627", created_at: "2009-09-29 14:58:21", updated_at: "2009-09-29 14:58:21", addressable_id: 9, addressable_type: "User", state_name: nil, type: "DeliveryAddress">

The addressable_id and addressable_type are both wrong – basically the DeliveryAddress can’t figure out what it’s attached to. Object reloading after saves results in a disassociated DeliveryAddress wandering around like something out of a Dostoyevsky novel.

Not having time today to fix Rails, my rather inelegant solution was to move the association up into User:

def User < ActiveRecord::Base
  has_one :delivery_address, :as => :addressable
end
def Customer < User
end

As Paul Graham once said:

SUVs, for example, would arguably be gross even if they ran on a fuel which would never run out and generated no pollution. SUVs are gross because they’re the solution to a gross problem. (How to make minivans look more masculine.)

So, in the same way, it’s a solution to a gross problem – but it works. The other (also crappy) alternative would have been to split out the Customer to not inherit from User, or perhaps to split out DeliveryAddress so it didn’t inherit from Address. This would be all out of proportion to what we’re trying to do (changing the inheritance hierarchy, adding database tables).

Dan had a sneaky solution to making sure that the lame delivery_address association never got used on the User:

validate_inclusion_of :delivery_address, :in => nil

This wins the sneaky code competition for the day, hands down.

Testing Using Cucumber/Webrat and Subdomains

Posted by dave
on Friday, September 25

A very quick note about testing using Cucumber and Webrat when using subdomains, in case it helps anyone (or perhaps my future self when I no longer remember this little detail in six months time).

Today I was working on a feature which had as one of its requirements that the user go to a special admin subdomain in order to log in to the admin system. This was blowing up all over the place, but we’ve now got it working alright. The first thing to realize is that Cucumber/Webrat need to be told about your subdomain. We first tried dumping the following into Cucumber’s env file:

def use_admin_subdomain
  host! "#{::ADMIN_SUBDOMAIN}.test.host" 
end

Our reasoning was that Rails normally runs its functional tests with a @request.host value of “test.host”, so if we wanted to shift things around, we figured we could just stick the admin subdomain on the front of that host and everything would be peachy. We put use_admin_subdomain at the top of our admin_steps, like so:

Given /^I am logged in as an admin user$/ do
  use_admin_subdomain
  Given %q{An admin user with username "admin@example.com" and password "password"}
  And %q{I log in with username "admin@example.com" and password "password"}
end

The result:

Then I should see "Thing was successfully created." # features/step_definitions/webrat_steps.rb:118
     expected the following element's content to include "Thing was successfully created.":
     You are being redirected. (Spec::Expectations::ExpectationNotMetError)
     features/admin_manage_thing.feature:16:in `Then I should see "Thing was successfully created."'
Failing Scenarios:
cucumber features/admin_manage_thing.feature:9 # Scenario: Create Thing

We scratched our heads and outputted the failing page into the browser. Weirdly, it just had a message in it saying “You are being redirected” with a link to the redirect. After doing a bit of research, we figured out that this happens because Cucumber/Webrat run tests in the www.example.com domain, so we were effectively trying to redirect in our tests from admin.test.host to www.example.com and Rails didn’t like doing that very much.

Changing our use_admin_subdomain method to do this instead worked great:

def use_admin_subdomain
  host! "#{::ADMIN_SUBDOMAIN}.example.com" 
end

This works because Rails is happy to do subdomain redirects and doesn’t display that weird “you are being redirected” page. And now, back to the code!