暇人じゃない

Devise 3.1 から Confirmation Token などがハッシュ化されるので Request Spec が失敗する

Devise の Confirmation, Reset password, Unlock で発行するトークンが、今まではそのままデータベースに格納されていましたが、v3.1.0 からハッシュ化されて格納されるようになりました。

他にも v3.1.0 では大きな変更がいくつかあります。Devise を使っている方は目を通しておきましょう。

Devise 3.1: Now with more secure defaults | Plataformatec Blog
http://blog.plataformatec.com.br/2013/08/devise-3-1-now-with-more-secure-defaults/

devise/CHANGELOG.md at v3.1.0 · plataformatec/devise
https://github.com/plataformatec/devise/blob/v3.1.0/CHANGELOG.md

Do not store confirmation, unlock and reset password tokens directly in the database. This means tokens previously stored in the database are no longer valid. You can reenable this temporarily by setting config.allowinsecuretokens_lookup = true in your configuration file. It is recommended to keep this configuration set to true just temporarily in your production servers only to aid migration

以前と同じように、ハッシュ化せずに格納するには config.allow_insecure_tokens_lookup = true とすれば良いのですが、v3.2 ではこの設定は廃止されるため、今のうちに対応しておいた方が良いです。

実装側では簡単に対応できたのですが(@resource.confirmation_token などとしている部分を @token に変更すれば OK)、以下のように、factory で Confirmation や Reset password のトークンを指定しているような Request Spec が失敗するようになりました。

FactoryGirl.define do
  factory :user do
    sequence(:email) { |n| "testuser#{n}@example.com" }    
    password "password"
    password_confirmation "password"

    factory :unconfirmed_user do
      confirmation_token 'testtoken'
      confirmed_at nil
      confirmation_sent_at { Time.now }
    end
  end
end

以下の Spec で使用している user.confirmation_token はハッシュ化されているため、テストが失敗してしまいます。

describe 'Confirmation' do
  let(:user) { FactoryGirl.create(:unconfirmed_user) }

  before do
    visit user_confirmation_path(confirmation_token: user.confirmation_token)
  end

  # ... 
end

そこで、ハッシュ化する前のトークンを指定するか、取り出す方法はないかな、と調べていたところ、以下のページが見つかりました。

ruby on rails - Upgrading to devise 3.1 => getting Reset password token is invalid - Stack Overflow
http://stackoverflow.com/questions/18661663/upgrading-to-devise-3-1-getting-reset-password-token-is-invalid

トークンを送信するメールのオブジェクトをパースしてトークンを取得する、という方法です。この Gist のようなコードでメールオブジェクトを取得してパースすれば良いようです。

Extract Devises “raw” tokens from outgoing email, since the tokens stored in the DB have been digested. (As for Devise 3.1)
https://gist.github.com/stevenharman/6227508

僕は以下のように、extract_token_from_mail の第一引数にメールオブジェクトを渡せるようにし、また、トークンをパースするための正規表現を変更しました。(元の正規表現では、改行以降のものまで含まれてしまうため)

spec/support/devise_mail_helpers.rb:

module DeviseMailHelpers
  def last_email
    ActionMailer::Base.deliveries[0]
  end

  # Can be used like:
  #  extract_token_from_email(last_email, :reset_password)
  def extract_token_from_email(mail, token_name)
    mail_body = mail.body.to_s
    mail_body[/#{token_name.to_s}_token=(.+)$/, 1]
  end
end

例えば Confirmation の Request Spec で以下のように使います:

describe 'Confirmation' do
  let(:user) { FactoryGirl.create(:unconfirmed_user) }

  let(:token) do
    extract_token_from_email(
      user.send_confirmation_instructions,
      :confirmation)
  end

  before do
    visit user_confirmation_path(confirmation_token: token)
  end

  # ...
end

Confirmation の send_confirmation_instructions を呼び出すことで、メールのオブジェクトが返ってきます。 これを extract_token_from_email でパースすることでトークンを取り出せます。

Password の send_reset_password_instructions や、Unlock の send_unlock_instructions ではメールオブジェクトではなく、ハッシュ化する前のトークンが返ってくるため、以下のように書けます。 (send_confirmation_instructions も同じようにトークンを返してくれれば良いのでは、という感じがしますが…)

describe 'Password' do
  let(:user) { FactoryGirl.create(:reset_password_user) }
  let(:token) { user.send_reset_password_instructions }

  before do
    visit edit_user_password_path(reset_password_token: token)
    fill_in 'user_password', with: 'password'
    fill_in 'user_password_confirmation', with: 'password'
    click_on '変更する'
  end

  # ...
end

他に良い方法があれば教えてください :)