Testing User Authentication
From the Fixture level
Our hypothetical Rails app: the Foo Club
Let's imagine that Foo Club wants you to build an application to display events. These events are of two types—public and private:
- Public events are visible to anyone visiting our application.
Say someone prospecting, to determine if the Club is a right fit.
- Private events are only visible to registered, authenticated members.
Also, only the admins should be able to edit, delete etc. events.
Authentication: minimal, yet powerful
Our app uses a standard email and password combination to authenticate the Club's members. It does this by leveraging
has_secure_password, which adds methods to set and authenticate against a
BCrypt password. This way, no plain text passwords are persisted in the database, but a
password_digest is what gets stored instead.
Fixtures are but YAML files, just better
As any fan of fixtures, we avoid spreading definitions of different types of members all across our tests. We isolate them in one place instead, in YAML files.
Say we want to model two different kinds of users, and we pick Jax as a member of the Foo Club and Tig, who's not convinced yet; he's still looking around:
# test/fixtures/users.yml member: name: Jackson Teller email: email@example.com visitor: name: Alexander Trager email: firstname.lastname@example.org
As a member, Jax needs a password, but as we mentioned earlier, these are never stored in plain text. Instead, we need to generate the
password_digest when the record is created, based on the unencrypted password. For a good example, let's see how Rails does it:
# … self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost) # …
One advantage of fixture files over classical YAML files is that we can inject dynamic values using ERB. We can now easily inherit the logic we learnt from Rails in our fixture. Maybe the only unknown left is that
cost: what values can it take?
When we're running our tests, and don't care that much about encryption strength; we want them to run fast. So how low can dial this down? Looking at
bcrypt, the minimum cost supported by the algorithm is 4. Then easy:
# test/fixtures/users.yml member: name: Jackson Teller email: email@example.com password_digest: <%= BCrypt::Password.create('password', cost: 4) %> # …
Keep it short and simple
Getting to know the big picture a little better will allow us to reduce our code one step further. This test confirms that in the test environment the minimum cost is used when computing the secure password. Armed with this knowledge, we can make our fixture file more succinct:
# test/fixtures/users.yml member: name: Jackson Teller email: firstname.lastname@example.org password_digest: <%= BCrypt::Password.create('password') %> # …
Now it's easy to go ahead writing our controller or integration tests, adding a helper method that automatically logs the member in, add different kinds of users (maybe an admin?) and check if they can delete an event while regular members, or simple visitors cannot. You name it!
Cover image credit: https://500px.com/photo/63010225/love-locks-by-karin-pezel