セキュリティ系の勉強・その他開発メモとか雑談. Twitter, ブログカテゴリ一覧
本ブログはあくまでセキュリティに関する情報共有の一環として作成したものであり,公開されているシステム等に許可なく実行するなど、違法な行為を助長するものではありません.

【Ruby】チュートリアルのform_forをform_withで書き換え (おまけ:capybaraでのテスト)

Ruby5.1前まではフォームを生成してくれるメソッドとして、form_forform_tagがあった。特にRubyチュートリアルでは2018/11/18現在、form_forを利用した方法を紹介しています。が、5.1以降はform_with推奨とのことなので、書き換えようと思いました。

form_with

APIドキュメントの翻訳版はこちらで読む事ができます。

techracho.bpsinc.jp


基準となるモデルがある場合

チュートリアルでは7.2.1で登場してます。Userモデルを作るフォームです。

<%= form_for(@user) do |f| %>
      <%= f.label :name %>
      <%= f.text_field :name %>

      <%= f.label :email %>
      <%= f.email_field :email %>

      <%= f.label :password %>
      <%= f.password_field :password %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation %>

      <%= f.submit "Create my account", class: "btn btn-primary" %>
    <% end %>


form_withを使用すると下のようにかけます。基本的に変わりませんが、model:でUserモデルを指定してあげます。チュートリアルの方を見ればわかりますが、コントローラの方で@user = User.newやってます。

<%=form_with model: @user do |f|%>
  <%= render 'shared/error_messages', object: @user%>

  <%=f.label :name%>
  <%=f.text_field :name,class:'form-control'%>

  <%=f.label :email%>
  <%= f.email_field :email,class:'form-control'%>

  <%=f.label :password%>
  <%= f.password_field :password,class:'form-control'%>

  <%=f.label :password_confirmation %>
  <%=f.password_field :password_confirmation,class:'form-control'%>

  <%=f.submit yield(:button_text),class:'btn btn-primary'%>
<%end%>


基準となるモデルがない場合

一方チュートリアル8.1.2では、上のような誰のフォームであるかをモデルで表わせず、sessionという言葉でまとめています。

<%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>


これは下のようにして書き換えられます。

    <%=form_with scope: :session, url:login_path do |f|%>

      <%=f.label :email%>
      <%=f.email_field :email, class:'form-control', id:'email'%>

      <%=f.label :password%>
      <%=f.password_field :password, class: 'form-control',id: 'password'%>

      <%=f.submit "Log in", class: "btn btn-primary"%>
    <%end%>


capybaraでのテスト

form_forでは、生成されるinputタグにid属性が付与されていました。一方form_withではid属性はオプションでつくようになっています。上の書き換えの例では、id属性にemailpasswordを指定しています。これはcapybaraでfill_inメソッドを使用してテストするためです。(他にもテスト方法はありますが、、)

#form_for
  <input class="form-control" id="session_email" name="session[email]" type="email" />
#form_with(id指定なし)
<input class="form-control" type="email" name="session[email]">
#form_with(id指定あり)
<input class="form-control" id="email" type="email" name="session[email]">


fill_inメソッドはidを参照するようで、id属性がないとエラーが起きます。変更前は以下のようにテストコードを書いていました。

#capybaraを使用しフォームに値を入れる
fill_in "Email",        with: "入力する値"

実はこれidがemailだと反応しません。_emailEmailが含まれていれば反応します。 なので、どっちも小文字でemailを指定するのが一番まるくおさまる気がします。もしくはid属性にEmailを指定してやればテストコードはそのままで大丈夫かもしれません。

ruby チュートリアル9章 メモ

SecureRandom.urlsafe_base64

Ruby標準ライブラリのメソッド。長さ22文字の文字列を生成してくれる。base64文字列なので64の22乘通りあるため、衝突の可能性も限りなく低い。

アクセサ

インスタンス変数のゲッターとセッターのこと。rubyのクラスのインスタンス変数にはアクセサが無いとアクセスすることができない。attr_accessorを使用することで自分で書くことなく利用できるようになる。

model(class)内でのselfの意味

チュートリアルでは、変数にselfをつける事でそのインスタンス内で共有が可能な変数を生成している。selfがない場合、ローカル変数になるとの記述があったので、おそらく定義したメソッドのスコープ内でのみ使用可能なものになってしまうはず。 ちなみにメソッドはクラスメソッド内でselfを使用するとクラスを指し、インスタンスメソッド内でselfはそのインスタンスになる。クラスメソッドはmodelクラスのレコードの更新や検索をする用に実装する。

class Article < ActiveRecord::Base
  def hoge       #インスタンスメソッド
  end

  def self.hoge  #クラスメソッド
  end

  def pdf       #インスタンスメソッド
    self.hoge        #インスタンスメソッドのhogeが呼ばれる
    hoge             #インスタンスメソッドのhogeが呼ばれる
    self.class.hoge  #こうするとクラスメソッドのhogeを呼べる
  end

  def self.pdf  #クラスメソッド
    self.hoge        #クラスメソッドのhogeが呼ばれる
    hoge             #クラスメソッドのhogeが呼ばれる
  end
end
# https://qiita.com/suzuki_koya/items/1553c405beeb73f83bbc


cookiesメソッド

1つのvalueとオプションのexpires(有効期限)から成っている。下のようにpermanentを使用すると有効期限が20年に設定される(よく20年設定が使われていた為)

cookies.permanent[:remember_token] = remember_token


それに伴いユーザIDの保存方法も変更する必要がある。signedを使用すると署名付きcookieを使うこと宣言し、permanentではcookieと同じ方法で永続化している。

#変更前
cookies[:user_id] = user.id

#変更後
cookies.permanent.signed[:user_id] = user.id

ユーザIDを取得する際はcookies.signed[:user_id]とする。

remember_me機能

remember_me機能をチェックの有無でクッキーがブラウザに保存されるかが決まる。これだけでこの機能が実現できているのは、セッションにはチェックに関わらず、変数にユーザ情報を保持させているから。一周回って仕組みが把握しにくくなってきた。

ログアウト時のcookieの削除

ログアウト処理にてcookieの値が正常に削除されたかをテストするには下のようにする。responseの中身のcookiesを覗かないと値が削除されていないので注意。

expect(response.cookies['user_id'].nil?).to eq false


インスタンス変数のテスト

インスタンス変数へはassignsを利用することでアクセスが可能になる。チュートリアルではこれを用いることで今までテストされていないかったインスタンス変数をテストした。

#@user内のremember_tokenへアクセスしている
expect(response.cookies['remember_token']).to eq assigns(:user).remember_token

ruby チュートリアル8章 メモ

セッションとクッキー

混同しがち(自分だけかも、)HTTPはステートレスなプロトコルなので、セッションのような物を実装するには、状態を保存するものが別に必要なる。それの一つがクッキー。クッキーはユーザのブラウザ内に保存される小さなテキスト。一方セッションはそもそも何かを継続的に行なっているものを指すらしく、接続し続けることもその中の一つと言う解釈でしょうか。

ログインページ

チュートリアルではこれから生成するセッションのコントローラがそのままログインページ系を司どるように作るそうです。言葉にするとわかりにくいので下にそれっぽいルーティングを示します。loginと言うコントローラを作るのではなく、セッションを開始終了のためのページがログイン/ログアウトってイメージですね。なお、セッションコントローラの処理はこれだけでよいので、generateで無駄なアクションを作ったり、resourcesで無駄にルーティングを作るのは避けます。

#config/route.rb
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'


renderメソッドとアクション

下のようにflashに値を格納した後にrenderを呼ぶと、flashは予期しない挙動をとる。と言うのも、rendernewを呼ぶとそれはアクションとは認められないため必然的にflashの生存時間が1アクション分伸びてしまう。

#app/controllers/sessions_controller.rb
 def create
    if ~~
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      flash[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end


flash.nowメソッドを利用することでこれを回避することができる。

 flash.now[:danger] = 'Invalid email/password combination'


findメソッドとfind_byメソッド

User.find(session[:user_id])でユーザ検索を行った場合、ユーザIDが存在していない状況(まだログインしていない、session[:user_id]の値がnilになる状態)だと、例外が発生する。一方User.find_by(id: session[:user_id])は、例外を発生せずnilを返す。

fixtureとFactorybot

チュートリアルではMinitest利用なのでfixtureを利用しているが、RSpecを使用していならFactorybotでカバーできる。個人的には下のようにしてチュートリアルのテストコードを実装しています。

#spec/controllers/user_controller_spec.rb
let(:user){build(:user)}
    let(:post_create){
      post( :create, params: {
        user: {
          name:   user.name,
          email:  user.email,
          password: user.password,
          password_confirmation: user.password_confirmation 
        }
    })}

    it 'ユーザが新規に生成される' do
      expect{post_create}.to change{User.count}.by(1)
    end

t-pot 観察 #6 明け方からアクセス増加

こんにちは。目立つログが残っていたので、メモしておきます。まず、今月のここまでのハニポ全体の記録をみてみます。
f:id:thinline196:20181111140554p:plain

今の所今月一のアクセス数です。では中身を見てみます。上のグラフと形が似てしまっていますが、今日(14時まで)の記録です。
f:id:thinline196:20181111140714p:plain

Cowrieが検知してますが、どうやら一度にあのアクセス量を受けていたわけでは無いようです。では国別とポート別に分けてみます。
f:id:thinline196:20181111140903p:plain
f:id:thinline196:20181111141018p:plain

詳細

中国からは211.143.198[.]237から16731程度のアクセス、アメリカからは1162.252.106[.]245から8300程度のアクセスで、どちらもsshへ向いてましたが時間帯は被っておらず。ロシアからはhttpへのアクセスですがこれもピークは他のアクセスとは被ってはおらず128.0.31[.]26から2000程度のアクセスでした。現地時間的にはアメリカとロシアからは夕方頃、中国は明け方頃ですね 。


3つの国からのアクセスは特に珍しくもなかったのですが、時間帯が近いのは珍しいと思います。(少なくとも私の環境だと) ここ最近だとMiraiが怪しいのかと思いますが、httpは確かmiraiの守備範囲ではなかった気がするので(IoTでは無い)、仮にmiraiのbotnetが動き出していたのなら、ロシアのは別攻撃ですね。そうすると中国とアメリカからのピーク時間が微妙に近いのもなんとなく腑に落ちるかもしれない?

ruby チュートリアル7章 メモ

サイトにデバッグ情報表示

下のコードをレイアウトのコードに挿入することで、描画されるページの状態を把握するのに役立つ情報をサイトに表示することができる。if~文は開発環境でのみデバッグ情報を表示するよう指定している。

#app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  .
  .
  .
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <%= yield %>
      <%= render 'layouts/footer' %>
      <%= debug(params) if Rails.env.development? %>
    </div>
  </body>
</html>


Sassのミックスイン

ミックスインを使用することでcssルールのグループをパッケージ化して簡単に複数要素に対して使用することができる。例えば、要素のサイズや見た目をいろんな場所で統一したい場合、あらかじめレイアウトをパッケージ化しておけば、そのパッケージをインポートするだけで、レイアウトを適用することできます。

#app/assets/stylesheets/custom.scss
@import "bootstrap-sprockets";
@import "bootstrap";

/* mixins, variables, etc. */

$gray-medium-light: #eaeaea;

@mixin box_sizing {
  -moz-box-sizing:    border-box;
  -webkit-box-sizing: border-box;
  box-sizing:         border-box;
}
.
.
.
/* miscellaneous */

.debug_dump {
  clear: both;
  float: left;
  width: 100%;
  margin-top: 45px;
  @include box_sizing;
}


RESTアーキテクチャの適用

データの作成、表示、更新、削除をリソースとして扱うこと。これらに対応する4つの操作(GET,POST,PATCH,DELETE)を各アクションに割り当てる必要がある。RailsのREST機能が有効だと、GETリクエストは自動的にshowアクションとして扱われる。この場合、リソースへの参照はリソース名とユニークIDをURIとして使用することになり、id=1のユーザの参照はusers/1となる。これをルーティングに設定するには下のようにする。

#config/routes.rb
Rails.application.routes.draw do
  root 'static_pages#home'
 . . .
  get  '/contact', to: 'static_pages#contact'
  get  '/signup',  to: 'users#new'
  resources :users
end


UsersリソースをRESTfulなアクションにした場合以下のようになる。

f:id:thinline196:20181106163627p:plain

debuggerメソッド

byebug gemのdebuggerメソッド。下のようにコード中に挟むとサーバを立ち上げたコンソール画面から、その場所のデバッグを行うことができる。

#app/controllers/users_controller.rb
class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
    debugger
  end

  def new
  end
end


Gravatar

プロフィール写真をアップロードして、指定したメールアドレスと関連付けることができ、画像パスを生成するだけでその画像を引っ張ってこれる。チュートリアルでは以下のようなヘルパーを作成して画像を引っ張ってきていたが、このチュートリアルに最適かは疑問。後々使うのであればまた別ですが。

  # 引数で与えられたユーザーのGravatar画像を返す
  def gravatar_for(user)
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end


オプション引数

メソッドを定義した際に予めデフォルト値を与えてやれば、引数に値を指定しなかった場合自動でデフォルトの値が使用される。もちろん値を渡した場合はその値が優先される。これをオプション引数といい下のように定義する。

  def gravatar_for(user, options = { size: 80 })
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    size = options[:size]
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end


またRuby2.0から導入されたキーワード引数を使用することで同じものをより短く実装が可能。

  def gravatar_for(user, size: 80)
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end


これのメソッドの呼び出し方は下のようにする。

<%gravatar_for @user%>  #デフォルトの80が適用される
<%gravatar_for @user, size:200%> #引数で渡した200がsizeに適用される。


form_forメソッド

form_forメソッドはActiveRecordのオブジェクトを取り込んで、そのオブジェクトの属性を使用してフォームを構築してくれる。

<%= form_for(@user) do |f| %>
  .
  .
<% end %>


fオブジェクトはHTMLフォームに対応するメソッドが呼び出されると、@userの属性を設定できる特別なHTMLを返す。labelの:nameシンボルは@userの属性になくてもエラーは出ない。シンボル名がそのままラベルに変換される。text_fieldの方はuserの属性に含まれるものをシンボルとして渡さないとエラーになる。

<%= f.label :name %>
<%= f.text_field :name %>

上を実行すると下のように展開される。

  <label for="user_name">Name</label>
  <input id="user_name" name="user[name]" type="text" />


text_fieldを使用すると一般的な入力フォームが、password_fieldを使用すると文字隠しが適用される入力フォームが生成されるなど、柔軟に対応できていることもわかります。
form自身のhtmlは以下のように生成される。

<form action="/users" class="new_user" id="new_user" method="post">


form_forは生成時に読み込んだ@userがUserクラスであり新しいUserであることを自動で認識し、actionとmethodを生成する。RESTfulな設計なのでpostはcreateに対応しており、Userクラスのcreateアクションが実行されるといった流れ。classとidはformヘルパーに対して特に直接的な結びつきは持たない。
-----------追記(2019/3/4)---------
チュートリアルでは、エラーの表示部分をパーシャルとして抽出しているのですが、チュートリアルのままだと@user変数にしか使えません。下のサイトを参考にどのフォームでもこのパーシャルを使い回せる様にしましょう。 qiita.com

= form_with(model: resource, as: resource_name, url: password_path(resource_name),local:true, html: { method: :post }) do |f|
  = render "shared/error_messages", model: f.object # 追加:modelで参照できる様にする
  .field
    = f.label :email
    br/
    = f.email_field :email, autofocus: true, id: "email" , autocomplete: "email", class: "form-control"
  .actions
    = f.submit I18n.t("devise.passwords.new.send_me_reset_password_instructions"), class: "btn btn-primary"
= render "devise/shared/links"

ごめんなさい、Slimでの記法になってます。

# _error_messages.html.slim
- if model.errors.any? # modelでオブジェクトの参照が可能
  .error-explanation.alert.alert-danger
    strong
      = I18n.t("errors.messages.not_saved", count: model.errors.count, resource: model.class.model_name.human.downcase)
    ul
      - model.errors.full_messages.each do |message|
        = message

-----------追記ここまで-----------

Strong Parameters

user情報入力後、その値を全てそのままUser生成に使用してしまったら、意図しないパラメータも更新されてしまう可能性がある。そのため、予め設定したものだけのみ、入力情報から取り出せるよにするべきである。

@user = User.new(params[:user]) #:userに格納された全ての情報を使用しようとする
@user = User.new(params.require(:user).permit(:name, :email, :password, :password_confirmation)) #許可した値だけ取り出される。


例えばcreateアクション時に簡単に呼び出せるようなメソッドをprivateに実装しておくことが考えられる。

  def create
    @user = User.new(user_params)
    if @user.save
      # 保存の成功をここで扱う。
    else
      render 'new'
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end


app/views/sharedディレクト

複数のビューで使用されるパーシャルはこの下に保存される。(習慣なので自動でやるわけではない)

pluralize

英語テキスト専用のヘルパー。一つ目の引数の値(数字)によって二つ目の文字を複数形に変更してくれる。

redirect_to

redirect_to @userredirect_to user_url(@user)と等価。これは以下の挙動と同じだが、あくまで挙動が同じだけ。route.rbでresourcesを指定していないものに対しては使用できない(user_urlがresourcesを使用した場合のみ利用可能になるから)

redirect_to "/users/#{@user.id}" # 条件付き(後述)
or
redirect_to user_url(id: @user.to_param)
or
redirect_to user_url(id: @user.id)


flash

ページが切り替わった時にユーザに向けて表示するメッセージを生成管理してくれるメソッド。コントローラのアクション定義の際にflashにシンボルと紐づけてメッセージを格納すると、次のアクションするまでその値を保持してくれる。(createアクション内で格納したメッセージは次のアクションで飛んだページでは残っているが、もう一度アクションをすると消える)

【Rails入門】flashの使い方まとめ | 侍エンジニア塾ブログ(Samurai Blog) - プログラミング入門者向けサイト
なお、次のアクションにも残さなかったり、逆に永続的に保持させることも可能。

content_tag

railsのヘルパー。content_tag(タグの名前)といった形で使用し、タグを動的に生成してくれる。チュートリアルではflash用のhtmlを見やすいように書き換えるために使用した。

#before
      <% flash.each do |message_type, message| %>
        <div class="alert alert-<%= message_type %>"><%= message %></div>
      <% end %>

#after
      <% flash.each do |message_type, message| %>
        <%= content_tag(:div, message, class: "alert alert-#{message_type}") %>
      <% end %>


今回のintegrationテストに関して

今回のサインアップのテストはチュートリアル上ではIntegrationテスト(featureテスト)の位置付けで行われていた。しかし、その内容はコントローラのテストとした方がよい可能性がある。
と言うのも、featureテストはcontrollerを挟んだviewをテストするイメージであり、今回のテストは完全にcontrollerのメソッド内で完結している処理だからである。
f:id:thinline196:20181109125000p:plain


では、viewのテストとは?一般にはviewのテストは書かないという風潮があるかもしれない。。何かを表示するにはほぼ必然的にcontrollerを経由し、それはつまりfeatureテストになる。(getしたページのタイトルが予期したものか等) またそもそもページに何のコンテンツが表示されているかのテストを始めると、保守コストが高すぎてやってられるか〜!となる話もあったりなかったり。
(念のためですが、書いている方もしっかりいらっしゃるので決して必要ないと言うことではありませんよ=> RSpec使ってERBに対するspecをどう書くか? - Qiita)


ちなみに、viewのヘルパーのテストは行う可能性が十分にあるので注意。