OmniAuthでログインを失敗したときのテストでエラーが出たときの対処法

OmniAuthを利用したログインのsystem specで、ログインに失敗した場合のテストが次のようなエラーになった。

  1) ユーザがログインする ログインに失敗したとき "ログインに失敗しました"と表示されること
     Failure/Error: click_link 'Twitterでログイン'
     
     OmniAuth::Error:
       invalid_credentials
     # ./spec/system/login_spec.rb:98:in `block (3 levels) in <top (required)>'

もとになったspecはこちら。

  context 'ログインに失敗したとき' do
    around do |example|
      original_mock_auth = OmniAuth.config.mock_auth[:twitter]
      OmniAuth.config.mock_auth[:twitter] = :invalid_credentials
      visit root_path
      click_link 'Twitterでログイン'
      example.run
      OmniAuth.config.mock_auth[:twitter] = original_mock_auth
    end

    it '"ログインに失敗しました"と表示されること' do
      expect(page).to have_content 'ログインに失敗しました'
    end
  end

これまでは普通にテストが通っていたのでなんだろうと思って調べた。

原因

つまりテストの実行順序の問題で、pumaの起動がOmniAuthのテストより前に来てしまうとRACK_ENVがdevelopmentになり期待していた挙動と異なる振る舞いをするようになってしまっていた。

とりあえず明示的に、次の行をconfig/rails_helper.rbに追加したところうまく動くようになった。

ENV['RACK_ENV'] ||= 'test'

所感

これ、自分と同じようにハマる人が出てくると思うのでライブラリ側でどうにかしたいのだけど、どのライブラリにどのようにPR投げるか悩ましいですね…

  • rspec-rails
  • puma
  • capybara
  • omniauth

のどれか。rspec-railsでrails_helper.rbのテンプレートをいじってRACK_ENVもRAILS_ENVと同様に設定するようにする、というのがいいだろうか。

おまけ

どこでRACK_ENVが設定されているか調べるのに使った雑なスクリプトを置いておきます

module DebugEnv
  def []=(*args)
    key = args.first
    if key == 'RAILS_ENV' || key == 'RACK_ENV'
      p args
      p caller
    end
    super
  end
end

ENV.singleton_class.prepend DebugEnv

あと、この手の環境変数関連のデバッグではspringは切っておいたほうが良さそう(ちゃんと調べきれてないけど、設定されていないはずのタイミングでRACK_ENVが設定されていて、これでかなり時間を奪われた><)

また、rake specrakeでテストを実行しているときにはこの問題は発生しなさそう。

rspec-rails/rspec.rake at master · rspec/rspec-rails

Turbolinks5でPOSTするときはajax経由のほうが良いのかも

Turbolinks5になってから「フォームでバリデーションエラーになったあとその画面をリロードしたときの挙動がTurbolinksを利用していないときと異なる」という問題がある。

2年半くらい前からIssueが立っている

Turbolinks doesn't recognize form re-submission [POST] and navigates instead [GET]. · Issue #251 · turbolinks/turbolinks

POSTしたあとの画面をリロードしようとすると、通常は「フォームの内容を再送信しますか?」みたいな確認ダイアログが出る。しかしTurbolinks経由だと単にいまのURLをGETで取得しようとする*1。これは、Turbolinksがhistory apiを使って履歴を操作しているのが原因。history apiに履歴を足すときはGETとしてのURLしか足せないので、現状のTurbolinksの仕様では対応するのは無理ぽい(放置されている原因の一つはこれだと思う)

そもそもTurbolinks(というかBasecamp)的には、formは全部js経由にしてしまう方針なので上記の問題で困っていない、というのもありそう。Railsでform_withを使ったとき、デフォルトではremote: trueになる。

普通はajaxリクエストでリダイレクトはできないのだけど、turbolinks(turbolinks-rails)ではajaxでpostを受け付けたときのハックが書かれていて、redirect_toされたときはTurbolinks.visitを使うようにしてリダイレクトを実現している。なので正常系だけならremote: trueで問題ない。

バリデーションエラーになったときの対応が面倒で、これは独自に定義をしないといけない。DHHはSJR(Server generated Javascript Response)でやればいいと書いてる。ようするにcreate.js.erbみたいなやつ。

Provide form_with as a new alternative to form_for/form_tag · Issue #25197 · rails/rails

わかるんだけどバリデーションエラーを表示するためだけにそれをやるのってめんどいのですよね。画一的に処理できるgemをつくればいいのか。すでに誰か作っていそうだからそれでも良さそう。

追記

だれも作ってなさそうなので作った turbolinksとform_withを便利に使うためのgemを作った - おもしろwebサービス開発日記

そのあとに似たようなのが作られていたことに気づいた>< jorgemanrubia/turbolinks_render: Support for render with Turbolinks in Rails controllers

*1:railsであればindexアクションのURLになるはず

同一モデルへの関連に対するdependentオプション

いつ書いたのか忘れたけど、下書きに書いてあったので公開

次のように、同じモデルに対してdependent: :destroyを設定してしまうと、もとのモデルを削除したときに二重にDELETEが発行されてしまう

class User < ApplicationRecord
  has_one :latest_post, -> { order(created_at: :desc) }, class_name: 'Post', dependent: :destroy
  has_many :posts, dependent: :destroy
end
 user.destroy
   (0.1ms)  begin transaction
  Post Load (0.3ms)  SELECT  "posts".* FROM "posts" WHERE "posts"."user_id" = ? ORDER BY "posts"."created_at" DESC LIMIT ?  [["user_id", 1], ["LIMIT", 1]]
  Post Destroy (0.4ms)  DELETE FROM "posts" WHERE "posts"."id" = ?  [["id", 2]]
  Post Destroy (0.1ms)  DELETE FROM "posts" WHERE "posts"."id" = ?  [["id", 1]]
  Post Destroy (0.1ms)  DELETE FROM "posts" WHERE "posts"."id" = ?  [["id", 2]]
  User Destroy (0.1ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 1]]
   (0.8ms)  commit transaction

dependent: :destroyは片方だけにしておいたほうがよさそう

capybaraでunicornを使うときの注意事項

  • capybaraで利用するテストサーバは変更可能
  • デフォルトだとwebrickだったはず?
  • Railsのsystem testだとpuma
  • unicornをproductionで使っているのであれば、unicornにしたほうがいいのでは?と思うタイミングがありやってみた

やり方

問題点

  • feature spec(system spec)でモックを使うことができないので使いづらい
  • puma(やwebrick)はテストのプロセスと同じプロセス内のスレッドで利用するので、モックの定義がテストサーバにも反映される
  • unicornは、最低でも2プロセスになる(マスタプロセスとワーカプロセス)
  • ワーカプロセスはテストのプロセスとは独立しているので、テストのプロセス内でモックを定義してもテストサーバには反映されない

まとめ

capybaraでunicornを使うには覚悟が必要

るりま編集方法のメモ

プログラミング言語 Ruby リファレンスマニュアルでリンク切れが見つかったので、PRしようと思って調べたことのメモ。

  • 内容はGitHubで管理されているrurema/doctree: Repository of Japanese Ruby reference manual
    • 普通にPRだせばよいみたい
  • wikiに古い記事がたくさんあって、何をどうしたら修正内容を確認できるのかよくわからない…
    • Rakefileがあったのでrakeタスクでhtmlファイルを確認できるのだろうと推測
    • bundle exec rake statichtml:2.5.0 のようにすると、ローカルの/private/tmp/html 配下にhtmlファイルが出力された
      • html出力用のdatabaseが/tmp/db-2.5.0のような場所にできる
      • 編集前にdatabaseを作ると、編集しても内容が反映されないっぽい

ということで編集した内容をローカルで確認できるところまでやった。がリンクの仕様がよくわからずPRまだ出せていない><

追記

PRだしました Fix broken link by willnet · Pull Request #1193 · rurema/doctree

Font Awesome5をRailsアプリケーションに導入した

Font Awesomeのバージョンが5になって、有料版と無料版の二パターンになった。

  • 4.7.0だと675アイコン
  • 5.0.6(無料版)だと929アイコン
  • 5.0.6(有料版)だと2316アイコン(無料版のアイコン含む)

Font Awesomeお世話になっているし、アイコン多いほうが嬉しいので課金した。リリース前だったので少し安くて$40だったはず(今は$60)。

Font Awesome5の導入の仕方はいくつかある。webサービスで使う場合は大きく次の2つ

  • jsでsvgを使う方法
  • cssとfontを使う方法

公式的にはjsの方がオススメとのこと。

js版のインストール方法と気をつける点

無料版はnpmパッケージとしても提供されているけど、有料版はパッケージがないみたいなので有料版を使う場合は公式サイトからファイルをダウンロードする必要がある。sprocketsもしくはwebpackerでも、どちらでもダウンロードしたものを読み込めば動いた。

js版、ファイル読み込み時にフォント用のクラスをsvgに置き換えるということをしているので、turbolinksを使っているとうまく動かない。次のようにturbolinks:loadイベントをフックしてsvgへの置き換え用のメソッドを実行してやるとうまく動く。

$(document).on('turbolinks:load', () => {
  window.FontAwesome.dom.i2svg()
})

css版のインストール

css版も一応検討したけど、sprockets経由で利用するにはcssからfontへのパスの解決が面倒(ファイル名にdigestが含まれてしまう)なのでjs版にした。4.7.0であればそのへん解決したfont-awesome-sassというgemがあるので、もしこれが5対応になったらこれをGemfileに入れるだけで良いのかもしれない。

(追記)5に対応したバージョンリリースされたみたい。

prmdを利用して空のレスポンスを表現する

prmdで生成されるドキュメントは、Response Exampleが自動的に生成されて便利。

f:id:willnet:20171229175710p:plain

しかし、何かを作成するときや何かを削除するときなど、レスポンスボディがなくても問題ないことを表すのが面倒。

prmd/link.md.erb at 928d9cb1c0fa1163bab5f3cb43c81eed4a147aa2 · interagent/prmd を読んで、どのようにResponse Exampleが生成されるかを調べて、空のレスポンスを表現する方法を調べてみた。

方法1: relをemptyにする

これが一番手っ取り早いけど、status codeが202に決め打ちになっている。

方法2: response_example を使う

次のようにresponse_exampleを指定するとResponse Examnpleをカスタマイズできる。若干面倒だけど現状ではこれしか方法がないように見える。

links:
- description: ユーザフォロー API
  href: "/api/user/{(%2Fschemata%2Fuser%23%2Fdefinitions%2Fid)}/follow"
  method: POST
  rel: create
  title: ユーザフォロー API
  schema:
    properties:
      access_token:
        "$ref": "/schemata/session#/definitions/access_token"
    required:
      - access_token
    type: object
  response_example:
    head: HTTP/1.1 201 Created
    body: ''