Sun, 19 Apr 09

Rails 2.3 and the ability to update created_at, created_on, updated_at and updated_on timestamps

The timing of the automatically generated timestamps and attribute assignment in rails 2.3 security note 1 was interesting to me because I’d just spent a while really trying to get my head around attr_accessible and attr_protected and the best way to test their effects in an app I was working on. Although I intend to write about the testing separately, the conclusion I came to was that I wanted to test the behaviour (more specifically, I was interested in what I could/couldn’t assign) of my objects and not that I was specifically using attr_accessible or attr_protected.

Reading Alex’s post made me wonder whether the rails API ever guaranteed that the timestamps would be readonly: If that was the case then I can see arguments for not wanting to test the behaviour of your objects (because you’re essentially testing the framework itself). I couldn’t find any mention of whether they’d specifically be readonly so chose to do a bit of digging. I created a script that allowed me to, with relative ease, run tests against rails apps created with different versions of rails. Using this I was able to test the mass assignment of timestamps (created_at, created_on, updated_at and updated_on) against multiple versions of rails. The conclusion I came to (and I’d love for other people to replicate my experiment and prove/disprove my results) was that the created_at and created_on pair of attributes have always (at least as far back as rails 1.0.0) been assignable, while the updated_at and updated_on attributes have only recently become assignable. While digging I managed to find the lighthouse ticket (#1612) that requested the ability to set the updated_* timestamps and the rails commit that closed that ticket.

Replicating my experiment

Get a copy of rails

$ cd /path/to/code
$ git clone git://github.com/rails/rails.git

Create a mysql database called rails_timestamps_test, and create a table within that database called people.

mysql> CREATE DATABASE rails_timestamps_test;
mysql> USE rails_timestamps_test;
mysql> CREATE TABLE people (id INTEGER AUTO_INCREMENT, created_on DATE, created_at DATETIME, updated_on DATE, updated_at DATETIME, PRIMARY KEY (id));

Get a copy of my test script

$ cd /path/to/code
$ svn co http://chrisroos.googlecode.com/svn/trunk/scratch/rails_timestamps_test

Run my script against rails 2.3 (run setup_rails_project.rb without any arguments to see the options) and hopefully see output similar to below (which means that the tests passed as expected).

$ cd /path/to/code/rails_timestamps_test
$ ruby setup_rails_project.rb 2.3.0 /path/to/code/rails

*** Removing the rails app at /path/to/code/rails_timestamps_test/projects/rails-app-2-3-0
*** Checking out the rails version tagged v2.3.0
HEAD is now at beca1f2... Template#mime_type should not use Mime::Type when Action Controller is not included
*** Creating the rails app at /path/to/code/rails_timestamps_test/projects/rails-app-2-3-0
*** Vendorising rails from /path/to/code/rails to /path/to/code/rails_timestamps_test/projects/rails-app-2-3-0
*** Generating rails_version_test.rb in /path/to/code/rails_timestamps_test/projects/rails-app-2-3-0/test/unit to ensure we are testing against the correct version of rails
*** Copying assets to the new rails app
****** Linking /path/to/code/rails_timestamps_test/assets/person_test.rb to /path/to/code/rails_timestamps_test/projects/rails-app-2-3-0/test/unit/person_test.rb
****** Linking /path/to/code/rails_timestamps_test/assets/person.rb to /path/to/code/rails_timestamps_test/projects/rails-app-2-3-0/app/models/person.rb
****** Linking /path/to/code/rails_timestamps_test/assets/database.yml to /path/to/code/rails_timestamps_test/projects/rails-app-2-3-0/config/database.yml
*** Running the rails version test to ensure that we're testing against the correct version of rails
Loaded suite test/unit/rails_version_test
Started
.
Finished in 0.000327 seconds.

1 tests, 1 assertions, 0 failures, 0 errors
*** Running the timestamps test
Loaded suite test/unit/person_test
Started
....
Finished in 0.078819 seconds.

4 tests, 8 assertions, 0 failures, 0 errors

Re-run the script against rails 2.2.2 (the last tag before 2.3) and you should see the same as above except for the last test which should now contain two failures.

$ cd /path/to/code/rails_timestamps_test
$ ruby setup_rails_project.rb 2.2.2 /path/to/code/rails

... some lines snipped ...
*** Running the timestamps test
Loaded suite test/unit/person_test
Started
..FF
Finished in 0.069429 seconds.

  1) Failure:
test_should_be_able_to_set_updated_at(PersonTest)
    [test/unit/person_test.rb:35:in `test_should_be_able_to_set_updated_at'
     /path/to/code/rails_timestamps_test/projects/rails-app-2-2-2/vendor/rails/activesupport/lib/active_support/testing/setup_and_teardown.rb:94:in `__send__'
     /path/to/code/rails_timestamps_test/projects/rails-app-2-2-2/vendor/rails/activesupport/lib/active_support/testing/setup_and_teardown.rb:94:in `run']:
FAIL: Couldn't persist the mass assigned updated_at attribute.
<Thu Jan 01 00:00:00 +0000 2009> expected but was
<Sun, 19 Apr 2009 09:23:46 UTC +00:00>.

  2) Failure:
test_should_be_able_to_set_updated_on(PersonTest)
    [test/unit/person_test.rb:28:in `test_should_be_able_to_set_updated_on'
     /path/to/code/rails_timestamps_test/projects/rails-app-2-2-2/vendor/rails/activesupport/lib/active_support/testing/setup_and_teardown.rb:94:in `__send__'
     /path/to/code/rails_timestamps_test/projects/rails-app-2-2-2/vendor/rails/activesupport/lib/active_support/testing/setup_and_teardown.rb:94:in `run']:
FAIL: Couldn't persist the mass assigned updated_on attribute.
<Thu, 01 Jan 2009> expected but was
<Sun Apr 19 09:23:46 UTC 2009>.

4 tests, 8 assertions, 2 failures, 0 errors

Re-run the script against any other tagged version of rails you wish.

A couple of notes about the script

If you specify an invalid tag (i.e. a tag that doesn’t exist in the rails repository) then the rails_version_test.rb will fail.

$ cd /path/to/code/rails_timestamps_test
$ ruby setup_rails_project.rb made.up.tag /path/to/code/rails

... some lines snipped ...
Loaded suite test/unit/rails_version_test
Started
F
Finished in 0.045543 seconds.

  1) Failure:
test_should_be_using_rails_made_up_tag(RailsVersionTest)
    [test/unit/rails_version_test.rb:6:in `test_should_be_using_rails_made_up_tag'
     /path/to/code/rails_timestamps_test/projects/rails-app-made-up-tag/vendor/rails/activesupport/lib/active_support/testing/setup_and_teardown.rb:94:in `__send__'
     /path/to/code/rails_timestamps_test/projects/rails-app-made-up-tag/vendor/rails/activesupport/lib/active_support/testing/setup_and_teardown.rb:94:in `run']:
<"made.up.tag"> expected but was
<"2.2.2">.

1 tests, 1 assertions, 1 failures, 0 errors

If you specify a tag of rails that is anything other than a 3 part version number (e.g. the four part version 2.3.2.1, or the release candidate 2.1.0_RC1) then the version test will fail because the Rails::VERSION constant only contains the MAJOR, MINOR and TINY components and that’s what we use to check the version our app uses.

The supplied database.yml file assumes a mysql database running on localhost that has a root user with no password. You can update this to reflect your actual environment.

The projects are created in /path/to/code/rails_timestamps_test/projects/ with a name like rails-app-x-x-x (major, minor, tiny).

In order to ensure we are testing against the correct version of rails, I symlink rails from /path/to/code/rails (after changing to the requested tag) into rails-app-x-x-x/vendor/rails. The problem here is that if you re-run the script with a different rails version and then attempt to run a test from your first project that it will be using the recently specified version of rails. Luckily, the rails_version_test will fail fast which means you shouldn’t have to waste time wondering why something is not working as expected.

I wonder whether it’d be useful to extract the ‘create a rails app from a specific version of rails’ functionality out of this script?

1 Alex MacCaw raised the issue in reference to number eight in the list of 10 cools things in Rails 2.3.