Wednesday, August 23, 2006

Testing XML-ish stuff in Rails

Hi fellow Railsians,

Today post is in answer to a question from Chris T on the Rails list. Chris asks:
"Probably dead obvious, but are there any assertions for easing testing
of xml output, both for builder templates (for RSS feed -- something
like a version of assert_tag) and for the new restful stuff."

I've seen this question come up several times in the past. The trouble with assert_tag and assert_no_tag is that they are hooked directly to the response object of your controllers. Which means you can only use them in functional or integration tests. You could fake out a response object, but there is a simpler solution, just recreate them. They are essentially a call to the find method of HTML::Document.

Here is what I did: Add the following to the end your test/test_helper.rb:

# Add more helper methods to be used by all tests here...
def assert_xml_tag(xml, conditions)
doc =
assert doc.find(conditions),
"expected tag, but no tag found matching #{conditions.inspect} in:\n#{xml.inspect}"

def assert_no_xml_tag(xml, conditions)
doc =
assert !doc.find(conditions),
"expected no tag, but found tag matching #{conditions.inspect} in:\n#{xml.inspect}"

I chose to use the actual value as the first parameter rather than the expected value because it allows for nicer looking paramter hash values. To use it, use just like assert_tag and assert_no_tag:

def test_xml_correct
assert_xml_tag "", :tag => 'z',
:parent => {:tag => 'y',
:parent => {:tag => 'x'}}

I usually have a default object under test with a single method to output. This allows for a micro-DSL-ish thing to reduce typing overhead:

def assert_render(conditions)
assert_xml_tag @thing.render, conditions

Tuesday, August 01, 2006

Simplified user roles in Rails

Usually, the discussion around user authentication and authorization in Rails revolves around an authentication plugin, engine or a login generator. Next, you might get into the types of users and this is where you might see a discussion about Roles, UserRoles and Users. There is plenty of tutorials about this usually describing an example of HABTM or has_many :through. But for simple cases, you can get the effect of user roles without much effort at all, just through normal Rails associations.

In addition to your 'users' table, create tables for each type of role. These can contain attributes specific to that type of user. For instance, you can have a admin type of user with specific flags allowing different types of CRUD access. A business contact user might have additional ways to be contacted (phone extension, cell phone, mail stop, etc.) For each table, add a user_id foreign key. In the models, make sure these all 'belongs_to :user'.

Then in the User model, add a 'has_one :admin' and 'has_one :business_contact' etc. for each associated table. The has_one association adds methods to the model to make it easy to query the relation:

user = User.find 1
if user.admin
# do admin stuff
... unless user.business_contact.nil?

Each user can participate in more than one role. This keeps the user login centralized and the authorization stuff very readable. Adding a new type only requires adding a new table and updating app/models/user.rb to add the has_one associtation.

This simple strategy might not be enough for your needs. It doesn't work if you anticipate adding user roles on the fly to a running system. In this case, you'd probably want to look into a permissions system tied to a HABTM based Role and UserRole model. I've only described a solution for a fixed amount of user types, more or less hard coded into the models.